@rellcodes16/devkit 1.0.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.
@@ -0,0 +1,666 @@
1
+ export function nodePackageJson(appname, options) {
2
+ const deps = {
3
+ express: "^4.19.2",
4
+ cors: "^2.8.5",
5
+ dotenv: "^16.4.5",
6
+ morgan: "^1.10.0",
7
+ helmet: "^7.1.0",
8
+ "cookie-parser": "^1.4.6",
9
+ "express-rate-limit": "^7.3.1",
10
+ "express-mongo-sanitize": "^2.2.0",
11
+ "xss-clean": "^0.1.4",
12
+ "serve-favicon": "^2.5.0",
13
+ validator: "^13.12.0",
14
+ uuid: "^10.0.0",
15
+ "http-errors": "^2.0.0",
16
+ };
17
+
18
+ if (options.mongo) deps["mongoose"] = "^8.4.3";
19
+ if (options.prisma) deps["@prisma/client"] = "^5.16.0";
20
+ if (options.turso) deps["@libsql/client"] = "^0.6.0";
21
+ if (options.pg) deps["postgres"] = "^3.4.4";
22
+ if (options.supabase) deps["@supabase/supabase-js"] = "^2.44.2";
23
+ if (options.redis) deps["ioredis"] = "^5.4.1";
24
+
25
+ if (options.drizzle) deps["drizzle-orm"] = "^0.32.0";
26
+
27
+ if (options.cloudinary) {
28
+ deps["cloudinary"] = "^2.3.1";
29
+ deps["multer"] = "^1.4.5-lts.1";
30
+ deps["multer-storage-cloudinary"] = "^4.0.0";
31
+ }
32
+
33
+ if (options.socket) deps["socket.io"] = "^4.7.5";
34
+
35
+ const scripts = {
36
+ dev: "nodemon src/index.js",
37
+ start: "node src/index.js",
38
+ };
39
+
40
+ if (options.prisma) {
41
+ scripts["db:migrate"] = "npx prisma migrate dev";
42
+ scripts["db:studio"] = "npx prisma studio";
43
+ scripts["db:generate"] = "npx prisma generate";
44
+ }
45
+
46
+ if (options.drizzle) {
47
+ scripts["db:push"] = "npx drizzle-kit push";
48
+ scripts["db:studio"] = "npx drizzle-kit studio";
49
+ }
50
+
51
+ return JSON.stringify(
52
+ {
53
+ name: appname,
54
+ version: "1.0.0",
55
+ description: "",
56
+ main: "src/index.js",
57
+ type: "module",
58
+ scripts,
59
+ dependencies: deps,
60
+ devDependencies: { nodemon: "^3.1.4" },
61
+ },
62
+ null,
63
+ 2
64
+ );
65
+ }
66
+
67
+ export function nodeIndexJs(options) {
68
+ const imports = [
69
+ `import express from "express";`,
70
+ `import cors from "cors";`,
71
+ `import morgan from "morgan";`,
72
+ `import helmet from "helmet";`,
73
+ `import dotenv from "dotenv";`,
74
+ `import cookieParser from "cookie-parser";`,
75
+ `import rateLimit from "express-rate-limit";`,
76
+ `import mongoSanitize from "express-mongo-sanitize";`,
77
+ `import xss from "xss-clean";`,
78
+ `import favicon from "serve-favicon";`,
79
+ `import { fileURLToPath } from "url";`,
80
+ `import path from "path";`,
81
+ `import routes from "./routes/index.js";`,
82
+ `import { errorHandler } from "./middleware/errorHandler.js";`,
83
+ `import { notFound } from "./middleware/notFound.js";`,
84
+ ];
85
+
86
+ if (options.mongo) imports.push(`import connectDB from "./config/db.js";`);
87
+ if (options.prisma) imports.push(`import prisma from "./config/db.js";`);
88
+ if (options.turso) imports.push(`import { db } from "./config/db.js";`);
89
+ if (options.pg) imports.push(`import sql from "./config/db.js";`);
90
+ if (options.supabase) imports.push(`import supabase from "./config/db.js";`);
91
+ if (options.redis) imports.push(`import redis from "./config/redis.js";`);
92
+
93
+ if (options.socket) {
94
+ imports.push(
95
+ `import { createServer } from "http";`,
96
+ `import { Server } from "socket.io";`
97
+ );
98
+ }
99
+
100
+ const socketSetup = options.socket
101
+ ? `
102
+ const httpServer = createServer(app);
103
+ const io = new Server(httpServer, {
104
+ cors: { origin: process.env.CLIENT_URL || "*", credentials: true },
105
+ });
106
+
107
+ io.on("connection", (socket) => {
108
+ console.log("Client connected:", socket.id);
109
+ socket.on("disconnect", () => console.log("Client disconnected:", socket.id));
110
+ });
111
+ `
112
+ : "";
113
+
114
+ const listenTarget = options.socket ? "httpServer" : "app";
115
+
116
+ const dbConnect = options.mongo
117
+ ? `\n// Connect to MongoDB\nawait connectDB();\n`
118
+ : "";
119
+
120
+ const rateLimitSetup = `
121
+ const limiter = rateLimit({
122
+ windowMs: 15 * 60 * 1000, // 15 minutes
123
+ max: 100,
124
+ standardHeaders: true,
125
+ legacyHeaders: false,
126
+ message: { error: "Too many requests, please try again later." },
127
+ });`;
128
+
129
+ return `${imports.join("\n")}
130
+
131
+ dotenv.config();
132
+
133
+ const __filename = fileURLToPath(import.meta.url);
134
+ const __dirname = path.dirname(__filename);
135
+
136
+ const app = express();
137
+ const PORT = process.env.PORT || 3000;
138
+ ${socketSetup}${dbConnect}${rateLimitSetup}
139
+
140
+ // Security middleware
141
+ app.use(helmet());
142
+ app.use(mongoSanitize());
143
+ app.use(xss());
144
+ app.use("/api", limiter);
145
+
146
+ // Core middleware
147
+ app.use(cors({ origin: process.env.CLIENT_URL || "*", credentials: true }));
148
+ app.use(morgan(process.env.NODE_ENV === "production" ? "combined" : "dev"));
149
+ app.use(express.json({ limit: "10kb" }));
150
+ app.use(express.urlencoded({ extended: true }));
151
+ app.use(cookieParser());
152
+ app.use(favicon(path.join(__dirname, "../public", "favicon.ico")));
153
+
154
+ // Routes
155
+ app.use("/api", routes);
156
+
157
+ // 404 + error handler
158
+ app.use(notFound);
159
+ app.use(errorHandler);
160
+
161
+ ${listenTarget}.listen(PORT, () => {
162
+ console.log(\`Server running on http://localhost:\${PORT}\`);
163
+ });
164
+ `;
165
+ }
166
+
167
+ export function nodeEnv(options) {
168
+ const lines = [
169
+ `PORT=3000`,
170
+ `NODE_ENV=development`,
171
+ `CLIENT_URL=http://localhost:5173`,
172
+ ``,
173
+ ];
174
+
175
+ if (options.mongo) {
176
+ lines.push(`# MongoDB`);
177
+ lines.push(`MONGODB_URI=mongodb://localhost:27017/mydb`);
178
+ lines.push(``);
179
+ }
180
+
181
+ if (options.prisma) {
182
+ lines.push(`# Database (Prisma)`);
183
+ lines.push(`DATABASE_URL=postgresql://user:password@localhost:5432/mydb`);
184
+ lines.push(``);
185
+ }
186
+
187
+ if (options.turso) {
188
+ lines.push(`# Turso (LibSQL)`);
189
+ lines.push(`TURSO_DATABASE_URL=libsql://your-db.turso.io`);
190
+ lines.push(`TURSO_AUTH_TOKEN=your-auth-token`);
191
+ lines.push(``);
192
+ }
193
+
194
+ if (options.pg) {
195
+ lines.push(`# PostgreSQL`);
196
+ lines.push(`DATABASE_URL=postgresql://user:password@localhost:5432/mydb`);
197
+ lines.push(``);
198
+ }
199
+
200
+ if (options.supabase) {
201
+ lines.push(`# Supabase`);
202
+ lines.push(`SUPABASE_URL=https://your-project.supabase.co`);
203
+ lines.push(`SUPABASE_ANON_KEY=your-anon-key`);
204
+ lines.push(`SUPABASE_SERVICE_ROLE_KEY=your-service-role-key`);
205
+ lines.push(``);
206
+ }
207
+
208
+ if (options.redis) {
209
+ lines.push(`# Redis`);
210
+ lines.push(`REDIS_URL=redis://localhost:6379`);
211
+ lines.push(``);
212
+ }
213
+
214
+ if (options.cloudinary) {
215
+ lines.push(`# Cloudinary`);
216
+ lines.push(`CLOUDINARY_CLOUD_NAME=your-cloud-name`);
217
+ lines.push(`CLOUDINARY_API_KEY=your-api-key`);
218
+ lines.push(`CLOUDINARY_API_SECRET=your-api-secret`);
219
+ lines.push(``);
220
+ }
221
+
222
+ lines.push(`# Auth`);
223
+ lines.push(`JWT_SECRET=change-this-to-a-long-random-secret`);
224
+ lines.push(`JWT_EXPIRES_IN=7d`);
225
+
226
+ return lines.join("\n");
227
+ }
228
+
229
+ export function nodeGitignore() {
230
+ return `node_modules/
231
+ .env
232
+ .env.local
233
+ dist/
234
+ *.log
235
+ .DS_Store
236
+ `;
237
+ }
238
+
239
+ export function nodeMongoConfig() {
240
+ return `import mongoose from "mongoose";
241
+
242
+ export default async function connectDB() {
243
+ try {
244
+ const conn = await mongoose.connect(process.env.MONGODB_URI);
245
+ console.log(\`MongoDB connected: \${conn.connection.host}\`);
246
+ } catch (err) {
247
+ console.error("MongoDB connection failed:", err.message);
248
+ process.exit(1);
249
+ }
250
+ }
251
+ `;
252
+ }
253
+
254
+ export function nodePrismaConfig() {
255
+ return `import { PrismaClient } from "@prisma/client";
256
+
257
+ const prisma = new PrismaClient({
258
+ log: process.env.NODE_ENV === "development" ? ["query", "warn", "error"] : ["error"],
259
+ });
260
+
261
+ export default prisma;
262
+ `;
263
+ }
264
+
265
+ // Turso — raw SQL client only (no Drizzle unless --drizzle is also passed)
266
+ export function nodeTursoConfig() {
267
+ return `import { createClient } from "@libsql/client";
268
+
269
+ const db = createClient({
270
+ url: process.env.TURSO_DATABASE_URL,
271
+ authToken: process.env.TURSO_AUTH_TOKEN,
272
+ });
273
+
274
+ // Usage: const { rows } = await db.execute("SELECT * FROM users WHERE id = ?", [id]);
275
+ export default db;
276
+ `;
277
+ }
278
+
279
+ // Turso + Drizzle (only when --drizzle flag is also set)
280
+ export function nodeTursoWithDrizzleConfig() {
281
+ return `import { createClient } from "@libsql/client";
282
+ import { drizzle } from "drizzle-orm/libsql";
283
+ import * as schema from "./schema.js";
284
+
285
+ const client = createClient({
286
+ url: process.env.TURSO_DATABASE_URL,
287
+ authToken: process.env.TURSO_AUTH_TOKEN,
288
+ });
289
+
290
+ export const db = drizzle(client, { schema });
291
+ `;
292
+ }
293
+
294
+ export function nodeTursoSchema() {
295
+ return `import { text, integer, sqliteTable } from "drizzle-orm/sqlite-core";
296
+
297
+ // Example table, replace or extend with your own models
298
+ export const users = sqliteTable("users", {
299
+ id: integer("id").primaryKey({ autoIncrement: true }),
300
+ email: text("email").notNull().unique(),
301
+ name: text("name"),
302
+ createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(() => new Date()),
303
+ });
304
+ `;
305
+ }
306
+
307
+ export function nodeDrizzleConfig(options) {
308
+ const isPg = options.pg;
309
+ return `import { defineConfig } from "drizzle-kit";
310
+ import dotenv from "dotenv";
311
+ dotenv.config();
312
+
313
+ export default defineConfig({
314
+ schema: "./src/config/schema.js",
315
+ out: "./drizzle",
316
+ dialect: "${isPg ? "postgresql" : "turso"}",
317
+ dbCredentials: {
318
+ ${isPg
319
+ ? ` url: process.env.DATABASE_URL,`
320
+ : ` url: process.env.TURSO_DATABASE_URL,\n authToken: process.env.TURSO_AUTH_TOKEN,`}
321
+ },
322
+ });
323
+ `;
324
+ }
325
+
326
+ export function nodePgConfig() {
327
+ return `import postgres from "postgres";
328
+
329
+ const sql = postgres(process.env.DATABASE_URL, {
330
+ max: 10, // connection pool size
331
+ idle_timeout: 20, // close idle connections after 20s
332
+ connect_timeout: 10, // fail fast if DB unreachable
333
+ });
334
+
335
+ // Usage: const users = await sql\`SELECT * FROM users WHERE id = \${id}\`;
336
+ export default sql;
337
+ `;
338
+ }
339
+
340
+ export function nodePgWithDrizzleConfig() {
341
+ return `import postgres from "postgres";
342
+ import { drizzle } from "drizzle-orm/postgres-js";
343
+ import * as schema from "./schema.js";
344
+
345
+ const client = postgres(process.env.DATABASE_URL, { max: 10 });
346
+ export const db = drizzle(client, { schema });
347
+ `;
348
+ }
349
+
350
+ export function nodePgSchema() {
351
+ return `import { pgTable, serial, text, varchar, timestamp } from "drizzle-orm/pg-core";
352
+
353
+ // Example table — replace or extend with your own models
354
+ export const users = pgTable("users", {
355
+ id: serial("id").primaryKey(),
356
+ email: varchar("email", { length: 255 }).notNull().unique(),
357
+ name: text("name"),
358
+ createdAt: timestamp("created_at").defaultNow(),
359
+ });
360
+ `;
361
+ }
362
+
363
+ // Supabase
364
+ export function nodeSupabaseConfig() {
365
+ return `import { createClient } from "@supabase/supabase-js";
366
+
367
+ // Public client — safe for read operations and auth flows
368
+ export const supabase = createClient(
369
+ process.env.SUPABASE_URL,
370
+ process.env.SUPABASE_ANON_KEY
371
+ );
372
+
373
+ // Admin client — full access, never expose to the browser
374
+ export const supabaseAdmin = createClient(
375
+ process.env.SUPABASE_URL,
376
+ process.env.SUPABASE_SERVICE_ROLE_KEY
377
+ );
378
+
379
+ export default supabase;
380
+ `;
381
+ }
382
+
383
+ // Redis
384
+ export function nodeRedisConfig() {
385
+ return `import Redis from "ioredis";
386
+
387
+ const redis = new Redis(process.env.REDIS_URL, {
388
+ maxRetriesPerRequest: 3,
389
+ enableReadyCheck: true,
390
+ lazyConnect: true,
391
+ });
392
+
393
+ redis.on("connect", () => console.log("Redis connected"));
394
+ redis.on("error", (err) => console.error("Redis error:", err.message));
395
+
396
+ // Helpers
397
+ export async function getCache(key) {
398
+ const data = await redis.get(key);
399
+ return data ? JSON.parse(data) : null;
400
+ }
401
+
402
+ export async function setCache(key, value, ttlSeconds = 300) {
403
+ await redis.set(key, JSON.stringify(value), "EX", ttlSeconds);
404
+ }
405
+
406
+ export async function deleteCache(key) {
407
+ await redis.del(key);
408
+ }
409
+
410
+ export default redis;
411
+ `;
412
+ }
413
+
414
+ export function nodePrismaSchema() {
415
+ return `generator client {
416
+ provider = "prisma-client-js"
417
+ }
418
+
419
+ datasource db {
420
+ provider = "postgresql"
421
+ url = env("DATABASE_URL")
422
+ }
423
+
424
+ // Add your models below
425
+ // model User {
426
+ // id String @id @default(uuid())
427
+ // email String @unique
428
+ // name String?
429
+ // createdAt DateTime @default(now())
430
+ // updatedAt DateTime @updatedAt
431
+ // }
432
+ `;
433
+ }
434
+
435
+ export function nodeCloudinaryConfig() {
436
+ return `import { v2 as cloudinary } from "cloudinary";
437
+ import { CloudinaryStorage } from "multer-storage-cloudinary";
438
+ import multer from "multer";
439
+
440
+ cloudinary.config({
441
+ cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
442
+ api_key: process.env.CLOUDINARY_API_KEY,
443
+ api_secret: process.env.CLOUDINARY_API_SECRET,
444
+ });
445
+
446
+ const storage = new CloudinaryStorage({
447
+ cloudinary,
448
+ params: {
449
+ folder: "uploads",
450
+ allowed_formats: ["jpg", "jpeg", "png", "webp", "gif"],
451
+ transformation: [{ width: 1200, crop: "limit" }],
452
+ },
453
+ });
454
+
455
+ export const upload = multer({ storage });
456
+ export default cloudinary;
457
+ `;
458
+ }
459
+
460
+ export function nodeErrorHandler() {
461
+ return `export function errorHandler(err, req, res, next) {
462
+ const status = err.status || err.statusCode || 500;
463
+ const message = err.message || "Internal Server Error";
464
+
465
+ if (process.env.NODE_ENV === "development") {
466
+ console.error(\`[ERROR \${status}]\`, err.stack);
467
+ }
468
+
469
+ res.status(status).json({
470
+ success: false,
471
+ error: message,
472
+ ...(process.env.NODE_ENV === "development" && { stack: err.stack }),
473
+ });
474
+ }
475
+ `;
476
+ }
477
+
478
+ export function nodeNotFound() {
479
+ return `export function notFound(req, res, next) {
480
+ res.status(404).json({
481
+ success: false,
482
+ error: \`Route not found — \${req.method} \${req.originalUrl}\`,
483
+ });
484
+ }
485
+ `;
486
+ }
487
+
488
+ export function nodeAsyncHandler() {
489
+ return `/**
490
+ * Wraps an async route handler and forwards errors to Express error middleware.
491
+ * Eliminates try/catch boilerplate in every controller.
492
+ *
493
+ * Usage:
494
+ * router.get("/", asyncHandler(async (req, res) => { ... }));
495
+ */
496
+ export const asyncHandler = (fn) => (req, res, next) =>
497
+ Promise.resolve(fn(req, res, next)).catch(next);
498
+ `;
499
+ }
500
+
501
+ export function nodeRoutes() {
502
+ return `import { Router } from "express";
503
+
504
+ const router = Router();
505
+
506
+ router.get("/health", (req, res) => {
507
+ res.json({ success: true, status: "ok", timestamp: new Date().toISOString() });
508
+ });
509
+
510
+ export default router;
511
+ `;
512
+ }
513
+
514
+ export function nodeUserModel(withCloudinary = false) {
515
+ const avatarField = withCloudinary
516
+ ? ` avatar: {
517
+ url: { type: String, default: "" },
518
+ publicId: { type: String, default: "" },
519
+ },`
520
+ : "";
521
+
522
+ return `import mongoose from "mongoose";
523
+ import validator from "validator";
524
+ import { v4 as uuidv4 } from "uuid";
525
+
526
+ const userSchema = new mongoose.Schema(
527
+ {
528
+ _id: { type: String, default: uuidv4 },
529
+ name: {
530
+ type: String,
531
+ required: [true, "Name is required"],
532
+ trim: true,
533
+ minlength: [2, "Name must be at least 2 characters"],
534
+ },
535
+ email: {
536
+ type: String,
537
+ required: [true, "Email is required"],
538
+ unique: true,
539
+ lowercase: true,
540
+ validate: [validator.isEmail, "Please provide a valid email"],
541
+ },
542
+ password: {
543
+ type: String,
544
+ required: [true, "Password is required"],
545
+ minlength: [8, "Password must be at least 8 characters"],
546
+ select: false,
547
+ },
548
+ role: {
549
+ type: String,
550
+ enum: ["user", "admin"],
551
+ default: "user",
552
+ },
553
+ ${avatarField}
554
+ isActive: { type: Boolean, default: true },
555
+ },
556
+ { timestamps: true }
557
+ );
558
+
559
+ // Strip password from JSON responses
560
+ userSchema.set("toJSON", {
561
+ transform: (doc, ret) => {
562
+ delete ret.password;
563
+ return ret;
564
+ },
565
+ });
566
+
567
+ const User = mongoose.model("User", userSchema);
568
+ export default User;
569
+ `;
570
+ }
571
+
572
+ export function nodeReadme(appname, options) {
573
+ const stackLines = [
574
+ "- Node.js + Express",
575
+ "- Helmet (security headers)",
576
+ "- CORS",
577
+ "- Morgan (logging)",
578
+ "- express-rate-limit",
579
+ "- express-mongo-sanitize",
580
+ "- xss-clean",
581
+ "- cookie-parser",
582
+ "- dotenv",
583
+ "- validator",
584
+ "- uuid",
585
+ "- serve-favicon",
586
+ ];
587
+
588
+ if (options.mongo) stackLines.push("- Mongoose (MongoDB)");
589
+ if (options.prisma) stackLines.push("- Prisma (PostgreSQL)");
590
+ if (options.turso) stackLines.push(options.drizzle ? "- Turso (LibSQL) + Drizzle ORM" : "- Turso (LibSQL) — raw SQL client");
591
+ if (options.pg) stackLines.push(options.drizzle ? "- postgres.js (PostgreSQL) + Drizzle ORM" : "- postgres.js (PostgreSQL)");
592
+ if (options.supabase) stackLines.push("- Supabase JS client");
593
+ if (options.redis) stackLines.push("- Redis (ioredis)");
594
+ if (options.cloudinary) stackLines.push("- Cloudinary + Multer");
595
+ if (options.socket) stackLines.push("- Socket.io");
596
+
597
+ const dbSteps = [];
598
+ if (options.mongo) dbSteps.push("# Set MONGODB_URI to your local or Atlas connection string");
599
+ if (options.prisma) dbSteps.push("npx prisma migrate dev # after setting DATABASE_URL");
600
+ if (options.turso && options.drizzle) dbSteps.push("npx drizzle-kit push # after setting TURSO credentials");
601
+ if (options.pg && options.drizzle) dbSteps.push("npx drizzle-kit push # after setting DATABASE_URL");
602
+ if (options.supabase) dbSteps.push("# Set SUPABASE_URL, SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY");
603
+ if (options.redis) dbSteps.push("# Make sure Redis is running: redis-server");
604
+
605
+ const modelSection = options.mongo ? " models/ # Mongoose models\n " : "";
606
+
607
+ const configLines = [" config/", " db.js # Database connection"];
608
+ if (options.redis) configLines.push(" redis.js # Redis client + cache helpers");
609
+ if (options.cloudinary) configLines.push(" cloudinary.js # Cloudinary config + multer");
610
+ if (options.drizzle) configLines.push(" schema.js # Drizzle table definitions");
611
+
612
+ const envRows = [
613
+ "| `PORT` | Server port (default 3000) |",
614
+ "| `NODE_ENV` | Environment (development / production) |",
615
+ "| `CLIENT_URL` | Allowed CORS origin |",
616
+ options.mongo && "| `MONGODB_URI` | MongoDB connection string |",
617
+ (options.prisma || options.pg) && "| `DATABASE_URL` | PostgreSQL connection string |",
618
+ options.turso && "| `TURSO_DATABASE_URL` | Turso database URL |",
619
+ options.turso && "| `TURSO_AUTH_TOKEN` | Turso auth token |",
620
+ options.supabase && "| `SUPABASE_URL` | Supabase project URL |",
621
+ options.supabase && "| `SUPABASE_ANON_KEY` | Supabase anon/public key |",
622
+ options.supabase && "| `SUPABASE_SERVICE_ROLE_KEY` | Supabase service role key (server only) |",
623
+ options.redis && "| `REDIS_URL` | Redis connection URL |",
624
+ options.cloudinary && "| `CLOUDINARY_CLOUD_NAME` | Cloudinary cloud name |",
625
+ options.cloudinary && "| `CLOUDINARY_API_KEY` | Cloudinary API key |",
626
+ options.cloudinary && "| `CLOUDINARY_API_SECRET` | Cloudinary API secret |",
627
+ "| `JWT_SECRET` | JWT signing secret |",
628
+ "| `JWT_EXPIRES_IN` | JWT expiry (e.g. 7d) |",
629
+ ].filter(Boolean).join("\n");
630
+
631
+ return `# ${appname}
632
+
633
+ Scaffolded with [devkit](https://github.com/rellcodes16/devkit).
634
+
635
+ ## Stack
636
+
637
+ ${stackLines.join("\n")}
638
+
639
+ ## Getting started
640
+
641
+ \`\`\`bash
642
+ cp .env.example .env
643
+ ${dbSteps.join("\n")}
644
+ npm run dev
645
+ \`\`\`
646
+
647
+ ## Project structure
648
+
649
+ \`\`\`
650
+ src/
651
+ routes/ # Express routers
652
+ controllers/ # Route handler logic
653
+ middleware/ # errorHandler, notFound, asyncHandler
654
+ ${modelSection}${configLines.join("\n")}
655
+ index.js # App entry point
656
+ public/
657
+ favicon.ico
658
+ \`\`\`
659
+
660
+ ## Environment variables
661
+
662
+ | Variable | Description |
663
+ |---|---|
664
+ ${envRows}
665
+ `;
666
+ }