better-ts-stack 1.1.2 → 1.1.4

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 (36) hide show
  1. package/README.md +16 -27
  2. package/dist/builder/configGenerator.d.ts +1 -0
  3. package/dist/builder/configGenerator.d.ts.map +1 -1
  4. package/dist/builder/configGenerator.js +41 -4
  5. package/dist/builder/configGenerator.js.map +1 -1
  6. package/dist/builder/index.d.ts.map +1 -1
  7. package/dist/builder/index.js +3 -0
  8. package/dist/builder/index.js.map +1 -1
  9. package/dist/builder/moduleSelector.d.ts.map +1 -1
  10. package/dist/builder/moduleSelector.js +2 -5
  11. package/dist/builder/moduleSelector.js.map +1 -1
  12. package/dist/output/nextSteps.d.ts.map +1 -1
  13. package/dist/output/nextSteps.js +22 -0
  14. package/dist/output/nextSteps.js.map +1 -1
  15. package/package.json +1 -1
  16. package/templates/backend/express/config.json +3 -3
  17. package/templates/backend/express/src/index.ts +19 -4
  18. package/templates/frontend/nextjs/config.json +1 -2
  19. package/templates/frontend/nextjs/next.config.ts.hbs +12 -0
  20. package/templates/frontend/tanstack/config.json +48 -49
  21. package/templates/modules/auth/express/src/controllers/authController.ts.hbs +62 -13
  22. package/templates/modules/auth/express/src/middleware/requireAuth.ts.hbs +26 -26
  23. package/templates/modules/auth/express/src/services/userStore.ts.hbs +22 -0
  24. package/templates/modules/docker/Dockerfile.hbs +133 -125
  25. package/templates/modules/docker/config.json +1 -1
  26. package/templates/modules/mongoose/{config.json → express/config.json} +2 -2
  27. package/templates/modules/mongoose/{src → express/src}/models/User.ts +48 -48
  28. package/templates/modules/mongoose/nextjs/config.json +16 -0
  29. package/templates/modules/mongoose/nextjs/lib/db.ts +36 -0
  30. package/templates/modules/mongoose/nextjs/models/User.ts +48 -0
  31. package/templates/modules/mongoose/tanstack/config.json +17 -0
  32. package/templates/modules/mongoose/tanstack/src/lib/db.ts +37 -0
  33. package/templates/modules/mongoose/tanstack/src/models/User.ts +48 -0
  34. package/templates/frontend/nextjs/next.config.ts +0 -8
  35. package/templates/modules/mongoose/src/lib/db.ts.hbs +0 -43
  36. /package/templates/modules/mongoose/{src → express/src}/lib/db.ts +0 -0
@@ -2,32 +2,67 @@ import { Request, Response } from "express";
2
2
  import bcrypt from "bcrypt";
3
3
  import { signToken } from "../lib/jwt";
4
4
  import { AuthenticatedRequest } from "../middleware/requireAuth";
5
- import { createUser, findUserByEmail, toPublicUser } from "../services/userStore";
5
+ import {
6
+ createUser,
7
+ findUserByEmail,
8
+ isDuplicateEmailError,
9
+ toPublicUser,
10
+ } from "../services/userStore";
6
11
 
7
12
  const SALT_ROUNDS = 10;
13
+ const MIN_PASSWORD_LENGTH = 8;
14
+ // bcrypt ignores bytes beyond 72, so longer passwords would be silently truncated
15
+ const MAX_PASSWORD_BYTES = 72;
8
16
 
17
+ // Request bodies are untrusted JSON, so fields may have any type
9
18
  interface RegisterBody {
10
- email?: string;
11
- password?: string;
12
- name?: string | null;
19
+ email?: unknown;
20
+ password?: unknown;
21
+ name?: unknown;
13
22
  }
14
23
 
15
24
  interface LoginBody {
16
- email?: string;
17
- password?: string;
25
+ email?: unknown;
26
+ password?: unknown;
27
+ }
28
+
29
+ // Returns a trimmed, lowercased email, or null when the value is not a usable string
30
+ function normalizeEmail(email: unknown): string | null {
31
+ if (typeof email !== "string") return null;
32
+ const normalized = email.trim().toLowerCase();
33
+ return normalized || null;
18
34
  }
19
35
 
20
36
  export async function register(
21
- req: Request<object, unknown, RegisterBody>,
37
+ req: Request<object, unknown, RegisterBody | undefined>,
22
38
  res: Response,
23
39
  ): Promise<void> {
24
- const { email, password, name } = req.body;
40
+ // Express 5 leaves req.body undefined when the request has no body
41
+ const body = req.body ?? {};
42
+ const email = normalizeEmail(body.email);
43
+ const password = body.password;
44
+ const name = body.name ?? null;
25
45
 
26
- if (!email || !password) {
46
+ if (!email || typeof password !== "string" || !password) {
27
47
  res.status(400).json({ error: "Email and password are required" });
28
48
  return;
29
49
  }
30
50
 
51
+ if (
52
+ password.length < MIN_PASSWORD_LENGTH ||
53
+ Buffer.byteLength(password, "utf8") > MAX_PASSWORD_BYTES
54
+ ) {
55
+ res.status(400).json({
56
+ error: `Password must be at least ${MIN_PASSWORD_LENGTH} characters and at most ${MAX_PASSWORD_BYTES} bytes`,
57
+ });
58
+ return;
59
+ }
60
+
61
+ if (name !== null && typeof name !== "string") {
62
+ res.status(400).json({ error: "Name must be a string" });
63
+ return;
64
+ }
65
+
31
66
  const existingUser = await findUserByEmail(email);
32
67
  if (existingUser) {
33
68
  res.status(409).json({ error: "User already exists" });
@@ -35,19 +70,33 @@ export async function register(
35
70
  }
36
71
 
37
72
  const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);
38
- const user = await createUser({ email, name, password: hashedPassword });
73
+
74
+ let user;
75
+ try {
76
+ user = await createUser({ email, name, password: hashedPassword });
77
+ } catch (error) {
78
+ // A concurrent request registered the same email after the check above
79
+ if (isDuplicateEmailError(error)) {
80
+ res.status(409).json({ error: "User already exists" });
81
+ return;
82
+ }
83
+ throw error;
84
+ }
85
+
39
86
  const token = signToken({ sub: user.id, email: user.email, name: user.name });
40
87
 
41
88
  res.status(201).json({ user: toPublicUser(user), token });
42
89
  }
43
90
 
44
91
  export async function login(
45
- req: Request<object, unknown, LoginBody>,
92
+ req: Request<object, unknown, LoginBody | undefined>,
46
93
  res: Response,
47
94
  ): Promise<void> {
48
- const { email, password } = req.body;
95
+ const body = req.body ?? {};
96
+ const email = normalizeEmail(body.email);
97
+ const password = body.password;
49
98
 
50
- if (!email || !password) {
99
+ if (!email || typeof password !== "string" || !password) {
51
100
  res.status(400).json({ error: "Email and password are required" });
52
101
  return;
53
102
  }
@@ -1,26 +1,26 @@
1
- import { NextFunction, Request, Response } from 'express';
2
- import { AuthTokenPayload, verifyToken } from '../lib/jwt';
3
-
4
- export interface AuthenticatedRequest extends Request {
5
- user?: AuthTokenPayload;
6
- }
7
-
8
- export function requireAuth(req: AuthenticatedRequest, res: Response, next: NextFunction): void {
9
- const authHeader = req.headers.authorization;
10
-
11
- if (!authHeader || !authHeader.startsWith('Bearer')) {
12
- res.status(401).json({ error: 'Authorization header missing or invalid' });
13
- return;
14
- }
15
-
16
- const token = authHeader.substring('Bearer '.length);
17
-
18
- try {
19
- const payload = verifyToken(token);
20
- req.user = payload;
21
- next();
22
- } catch (error) {
23
- console.error('JWT verification failed:', error);
24
- res.status(401).json({ error: 'Invalid or expired token' });
25
- }
26
- }
1
+ import { NextFunction, Request, Response } from 'express';
2
+ import { AuthTokenPayload, verifyToken } from '../lib/jwt';
3
+
4
+ export interface AuthenticatedRequest extends Request {
5
+ user?: AuthTokenPayload;
6
+ }
7
+
8
+ export function requireAuth(req: AuthenticatedRequest, res: Response, next: NextFunction): void {
9
+ const authHeader = req.headers.authorization;
10
+
11
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
12
+ res.status(401).json({ error: 'Authorization header missing or invalid' });
13
+ return;
14
+ }
15
+
16
+ const token = authHeader.substring('Bearer '.length);
17
+
18
+ try {
19
+ const payload = verifyToken(token);
20
+ req.user = payload;
21
+ next();
22
+ } catch (error) {
23
+ console.error('JWT verification failed:', error);
24
+ res.status(401).json({ error: 'Invalid or expired token' });
25
+ }
26
+ }
@@ -77,6 +77,14 @@ export {{#unless (eq database 'none')}}async {{/unless}}function createUser(data
77
77
  {{/if}}
78
78
 
79
79
  {{#if (eq database 'none')}}
80
+ // Enforce unique emails like a database constraint would, so a sign-up that
81
+ // raced past the findUserByEmail check is rejected instead of duplicated
82
+ if (users.some((candidate) => candidate.email === data.email)) {
83
+ return Promise.reject(
84
+ Object.assign(new Error('User already exists'), { code: 'DUPLICATE_EMAIL' }),
85
+ );
86
+ }
87
+
80
88
  const newUser: AuthUser = {
81
89
  id: crypto.randomUUID(),
82
90
  email: data.email,
@@ -89,6 +97,20 @@ export {{#unless (eq database 'none')}}async {{/unless}}function createUser(data
89
97
  {{/if}}
90
98
  }
91
99
 
100
+ // Unique-constraint error codes: Prisma, PostgreSQL (Drizzle wraps it in `cause`),
101
+ // MongoDB, and the in-memory store
102
+ const DUPLICATE_KEY_CODES = new Set<unknown>(['P2002', '23505', 11000, 'DUPLICATE_EMAIL']);
103
+
104
+ // True when an insert failed because the email is already taken (e.g. concurrent sign-ups)
105
+ export function isDuplicateEmailError(error: unknown): boolean {
106
+ let current = error;
107
+ for (let depth = 0; depth < 5 && current && typeof current === 'object'; depth++) {
108
+ if (DUPLICATE_KEY_CODES.has((current as { code?: unknown }).code)) return true;
109
+ current = (current as { cause?: unknown }).cause;
110
+ }
111
+ return false;
112
+ }
113
+
92
114
  export function toPublicUser(user: AuthUser): Omit<AuthUser, 'password'> {
93
115
  const { id, email, name } = user;
94
116
  return { id, email, name };
@@ -1,126 +1,134 @@
1
- {{#if (eq framework 'nextjs')}}
2
- # Base stage
3
- FROM node:24-alpine AS base
4
- WORKDIR /app
5
- RUN apk add --no-cache libc6-compat
6
-
7
- # Dependencies stage
8
- FROM base AS deps
9
- COPY package*.json ./
10
- RUN if [ -f package-lock.json ]; then npm ci; else npm install; fi
11
-
12
- # Build stage
13
- FROM base AS builder
14
- COPY --from=deps /app/node_modules ./node_modules
15
- COPY . .
16
- {{#if (eq database 'prisma')}}
17
- RUN DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres" npm run build
18
- {{else if (eq database 'mongoose')}}
19
- RUN MONGODB_URI="mongodb://localhost:27017/build" npm run build
20
- {{else}}
21
- RUN npm run build
22
- {{/if}}
23
-
24
- # Production stage
25
- FROM node:24-alpine AS runner
26
- WORKDIR /app
27
- ENV NODE_ENV=production
28
-
29
- RUN addgroup --system --gid 1001 nodejs && \
30
- adduser --system --uid 1001 nextjs
31
-
32
- COPY --from=builder /app/public ./public
33
- COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
34
- COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
35
-
36
- USER nextjs
37
- EXPOSE {{port}}
38
- ENV PORT={{port}} HOSTNAME=0.0.0.0
39
-
40
- CMD ["node", "server.js"]
41
-
42
- {{else if (eq framework 'tanstack')}}
43
- # Build stage
44
- FROM node:24-alpine AS builder
45
- WORKDIR /app
46
-
47
- COPY package*.json ./
48
- RUN if [ -f package-lock.json ]; then npm ci; else npm install; fi
49
-
50
- COPY . .
51
-
52
- # Only build if package.json has a build script
53
- {{#if (eq database 'prisma')}}
54
- RUN DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres" npm run build
55
- {{else if (eq database 'mongoose')}}
56
- RUN MONGODB_URI="mongodb://localhost:27017/build" npm run build
57
- {{else}}
58
- RUN npm run build
59
- {{/if}}
60
-
61
- # Production stage
62
- FROM node:24-alpine
63
- WORKDIR /app
64
- ENV NODE_ENV=production
65
-
66
- # Create non-root user for security
67
- RUN addgroup --system --gid 1001 nodejs && \
68
- adduser --system --uid 1001 tanstackuser
69
-
70
- COPY package*.json ./
71
- RUN if [ -f package-lock.json ]; then npm ci --omit=dev; else npm install --omit=dev; fi
72
-
73
- # Copy built output (dist/server/server.js + dist/client)
74
- COPY --from=builder --chown=tanstackuser:nodejs /app/dist ./dist
75
-
76
- # Include the Prisma client generated during the build stage
77
- {{#if (eq database 'prisma')}}
78
- COPY --from=builder --chown=tanstackuser:nodejs /app/generated ./generated
79
- {{/if}}
80
-
81
- USER tanstackuser
82
- EXPOSE {{port}}
83
- ENV PORT={{port}}
84
-
85
- CMD ["node_modules/.bin/srvx", "--prod", "-s", "../client", "dist/server/server.js"]
86
-
87
- {{else}}
88
- # Build stage
89
- FROM node:24-alpine AS builder
90
- WORKDIR /app
91
-
92
- COPY package*.json ./
93
- RUN if [ -f package-lock.json ]; then npm ci; else npm install; fi
94
-
95
- COPY . .
96
-
97
- # Only build if package.json has a build script
98
- {{#if (eq database 'prisma')}}
99
- RUN DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres" npm run build
100
- {{else if (eq database 'mongoose')}}
101
- RUN MONGODB_URI="mongodb://localhost:27017/build" npm run build
102
- {{else}}
103
- RUN npm run build
104
- {{/if}}
105
-
106
- # Production stage
107
- FROM node:24-alpine
108
- WORKDIR /app
109
- ENV NODE_ENV=production
110
-
111
- # Create non-root user for security
112
- RUN addgroup --system --gid 1001 nodejs && \
113
- adduser --system --uid 1001 expressuser
114
-
115
- COPY package*.json ./
116
- RUN if [ -f package-lock.json ]; then npm ci --omit=dev; else npm install --omit=dev; fi
117
-
118
- # Copy built files (if TypeScript) or source files (if JavaScript)
119
- COPY --from=builder --chown=expressuser:nodejs /app/dist ./dist
120
-
121
- USER expressuser
122
- EXPOSE {{port}}
123
- ENV PORT={{port}}
124
-
125
- CMD ["node", "dist/index.js"]
1
+ {{#if (eq framework 'nextjs')}}
2
+ # Base stage
3
+ FROM node:24-alpine AS base
4
+ WORKDIR /app
5
+ RUN apk add --no-cache libc6-compat
6
+
7
+ # Dependencies stage
8
+ FROM base AS deps
9
+ COPY package*.json ./
10
+ RUN if [ -f package-lock.json ]; then npm ci; else npm install; fi
11
+
12
+ # Build stage
13
+ FROM base AS builder
14
+ COPY --from=deps /app/node_modules ./node_modules
15
+ COPY . .
16
+ {{#if useAuth}}
17
+ # next build loads the auth route, which initializes Better Auth. The build gets
18
+ # a placeholder secret for this command only; the real one is supplied at runtime.
19
+ {{/if}}
20
+ {{#if (eq database 'prisma')}}
21
+ RUN {{#if useAuth}}BETTER_AUTH_SECRET="build-only-placeholder-secret-not-used-at-runtime" {{/if}}DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres" npm run build
22
+ {{else if (eq database 'mongoose')}}
23
+ RUN {{#if useAuth}}BETTER_AUTH_SECRET="build-only-placeholder-secret-not-used-at-runtime" {{/if}}MONGODB_URI="mongodb://localhost:27017/build" npm run build
24
+ {{else}}
25
+ RUN {{#if useAuth}}BETTER_AUTH_SECRET="build-only-placeholder-secret-not-used-at-runtime" {{/if}}npm run build
26
+ {{/if}}
27
+
28
+ # Production stage
29
+ FROM node:24-alpine AS runner
30
+ WORKDIR /app
31
+ ENV NODE_ENV=production
32
+
33
+ RUN addgroup --system --gid 1001 nodejs && \
34
+ adduser --system --uid 1001 nextjs
35
+
36
+ COPY --from=builder /app/public ./public
37
+ COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
38
+ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
39
+
40
+ USER nextjs
41
+ EXPOSE {{port}}
42
+ ENV PORT={{port}} HOSTNAME=0.0.0.0
43
+
44
+ CMD ["node", "server.js"]
45
+
46
+ {{else if (eq framework 'tanstack')}}
47
+ # Build stage
48
+ FROM node:24-alpine AS builder
49
+ WORKDIR /app
50
+
51
+ COPY package*.json ./
52
+ RUN if [ -f package-lock.json ]; then npm ci; else npm install; fi
53
+
54
+ COPY . .
55
+
56
+ # Only build if package.json has a build script
57
+ {{#if (eq database 'prisma')}}
58
+ RUN DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres" npm run build
59
+ {{else if (eq database 'mongoose')}}
60
+ RUN MONGODB_URI="mongodb://localhost:27017/build" npm run build
61
+ {{else}}
62
+ RUN npm run build
63
+ {{/if}}
64
+
65
+ # Production stage
66
+ FROM node:24-alpine
67
+ WORKDIR /app
68
+ ENV NODE_ENV=production
69
+
70
+ # Create non-root user for security
71
+ RUN addgroup --system --gid 1001 nodejs && \
72
+ adduser --system --uid 1001 tanstackuser
73
+
74
+ COPY package*.json ./
75
+ # Clear the npm cache in the same layer so it does not ship in the image
76
+ RUN if [ -f package-lock.json ]; then npm ci --omit=dev; else npm install --omit=dev; fi \
77
+ && npm cache clean --force
78
+
79
+ # Copy built output (dist/server/server.js + dist/client)
80
+ COPY --from=builder --chown=tanstackuser:nodejs /app/dist ./dist
81
+
82
+ # Include the Prisma client generated during the build stage
83
+ {{#if (eq database 'prisma')}}
84
+ COPY --from=builder --chown=tanstackuser:nodejs /app/generated ./generated
85
+ {{/if}}
86
+
87
+ USER tanstackuser
88
+ EXPOSE {{port}}
89
+ ENV PORT={{port}}
90
+
91
+ CMD ["node_modules/.bin/srvx", "--prod", "-s", "../client", "dist/server/server.js"]
92
+
93
+ {{else}}
94
+ # Build stage
95
+ FROM node:24-alpine AS builder
96
+ WORKDIR /app
97
+
98
+ COPY package*.json ./
99
+ RUN if [ -f package-lock.json ]; then npm ci; else npm install; fi
100
+
101
+ COPY . .
102
+
103
+ # Only build if package.json has a build script
104
+ {{#if (eq database 'prisma')}}
105
+ RUN DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres" npm run build
106
+ {{else if (eq database 'mongoose')}}
107
+ RUN MONGODB_URI="mongodb://localhost:27017/build" npm run build
108
+ {{else}}
109
+ RUN npm run build
110
+ {{/if}}
111
+
112
+ # Production stage
113
+ FROM node:24-alpine
114
+ WORKDIR /app
115
+ ENV NODE_ENV=production
116
+
117
+ # Create non-root user for security
118
+ RUN addgroup --system --gid 1001 nodejs && \
119
+ adduser --system --uid 1001 expressuser
120
+
121
+ COPY package*.json ./
122
+ # Clear the npm cache in the same layer so it does not ship in the image
123
+ RUN if [ -f package-lock.json ]; then npm ci --omit=dev; else npm install --omit=dev; fi \
124
+ && npm cache clean --force
125
+
126
+ # Copy built files (if TypeScript) or source files (if JavaScript)
127
+ COPY --from=builder --chown=expressuser:nodejs /app/dist ./dist
128
+
129
+ USER expressuser
130
+ EXPOSE {{port}}
131
+ ENV PORT={{port}}
132
+
133
+ CMD ["node", "dist/index.js"]
126
134
  {{/if}}
@@ -7,7 +7,7 @@
7
7
  "devDependencies": {},
8
8
  "scripts": {
9
9
  "docker:build": "docker build -t {{projectName}} .",
10
- "docker:up": "docker compose up -d",
10
+ "docker:up": "docker compose up -d --build",
11
11
  "docker:down": "docker compose down",
12
12
  "docker:logs": "docker compose logs -f"
13
13
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "mongoose",
3
3
  "name": "Mongoose (MongoDB)",
4
- "description": "MongoDB database integration using Mongoose ODM",
4
+ "description": "MongoDB database integration for Express using Mongoose ODM",
5
5
  "type": "database",
6
6
  "dependencies": {
7
7
  "mongoose": "^9.9.5",
@@ -12,5 +12,5 @@
12
12
  "envVars": {
13
13
  "MONGODB_URI": "mongodb://localhost:27017/myapp"
14
14
  },
15
- "templateFiles": ["src/lib/db.ts.hbs"]
15
+ "templateFiles": []
16
16
  }
@@ -1,48 +1,48 @@
1
- import mongoose, { type Model, Schema } from "mongoose";
2
-
3
- /**
4
- * User document interface
5
- */
6
- export interface IUser {
7
- email: string;
8
- name?: string | null;
9
- password: string;
10
- createdAt: Date;
11
- updatedAt: Date;
12
- }
13
-
14
- /**
15
- * User schema definition
16
- */
17
- const userSchema = new Schema<IUser>(
18
- {
19
- email: {
20
- type: String,
21
- required: true,
22
- unique: true,
23
- lowercase: true,
24
- trim: true,
25
- },
26
- name: {
27
- type: String,
28
- trim: true,
29
- },
30
- password: {
31
- type: String,
32
- required: true,
33
- minlength: 6,
34
- },
35
- },
36
- {
37
- timestamps: true,
38
- }
39
- );
40
-
41
- /**
42
- * User model
43
- */
44
- const User =
45
- (mongoose.models.User as Model<IUser> | undefined) ??
46
- mongoose.model<IUser>("User", userSchema);
47
-
48
- export default User;
1
+ import mongoose, { type Model, Schema } from "mongoose";
2
+
3
+ /**
4
+ * User document interface
5
+ */
6
+ export interface IUser {
7
+ email: string;
8
+ name?: string | null;
9
+ password: string;
10
+ createdAt: Date;
11
+ updatedAt: Date;
12
+ }
13
+
14
+ /**
15
+ * User schema definition
16
+ */
17
+ const userSchema = new Schema<IUser>(
18
+ {
19
+ email: {
20
+ type: String,
21
+ required: true,
22
+ unique: true,
23
+ lowercase: true,
24
+ trim: true,
25
+ },
26
+ name: {
27
+ type: String,
28
+ trim: true,
29
+ },
30
+ password: {
31
+ type: String,
32
+ required: true,
33
+ minlength: 6,
34
+ },
35
+ },
36
+ {
37
+ timestamps: true,
38
+ }
39
+ );
40
+
41
+ /**
42
+ * User model
43
+ */
44
+ const User =
45
+ (mongoose.models.User as Model<IUser> | undefined) ??
46
+ mongoose.model<IUser>("User", userSchema);
47
+
48
+ export default User;
@@ -0,0 +1,16 @@
1
+ {
2
+ "id": "mongoose",
3
+ "name": "Mongoose (MongoDB)",
4
+ "description": "MongoDB database integration for Next.js using Mongoose ODM",
5
+ "type": "database",
6
+ "dependencies": {
7
+ "mongoose": "^9.9.5",
8
+ "mongodb": "^7.6.0"
9
+ },
10
+ "devDependencies": {},
11
+ "scripts": {},
12
+ "envVars": {
13
+ "MONGODB_URI": "mongodb://localhost:27017/myapp"
14
+ },
15
+ "templateFiles": []
16
+ }
@@ -0,0 +1,36 @@
1
+ import mongoose from "mongoose";
2
+
3
+ // Reuse one connection across hot reloads and concurrent requests
4
+ const globalForMongoose = globalThis as unknown as {
5
+ mongooseConnection?: Promise<typeof mongoose>;
6
+ };
7
+
8
+ /**
9
+ * Connects Mongoose once and returns the shared connection.
10
+ * Call this in server code before querying Mongoose models.
11
+ */
12
+ export async function connectDB(): Promise<typeof mongoose> {
13
+ const mongoUri = process.env.MONGODB_URI;
14
+
15
+ if (!mongoUri) {
16
+ throw new Error("MONGODB_URI environment variable is not defined");
17
+ }
18
+
19
+ globalForMongoose.mongooseConnection ??= mongoose
20
+ .connect(mongoUri)
21
+ .catch((error: unknown) => {
22
+ // Let the next call retry instead of caching the failure
23
+ globalForMongoose.mongooseConnection = undefined;
24
+ throw error;
25
+ });
26
+
27
+ return globalForMongoose.mongooseConnection;
28
+ }
29
+
30
+ /**
31
+ * Gracefully close the MongoDB connection
32
+ */
33
+ export async function disconnectDB(): Promise<void> {
34
+ await mongoose.connection.close();
35
+ globalForMongoose.mongooseConnection = undefined;
36
+ }