@yoms/create-monorepo 1.1.0 → 2.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.
package/dist/index.js CHANGED
@@ -6,7 +6,9 @@ import { cac } from "cac";
6
6
  // src/commands/create.ts
7
7
  import path7 from "path";
8
8
  import ora from "ora";
9
+ import chalk from "chalk";
9
10
  import { execa as execa2 } from "execa";
11
+ import enquirer4 from "enquirer";
10
12
 
11
13
  // src/utils/file-ops.ts
12
14
  import fs from "fs-extra";
@@ -74,6 +76,15 @@ var logger = {
74
76
 
75
77
  // src/utils/package-manager.ts
76
78
  import { execa } from "execa";
79
+ async function detectPackageManager() {
80
+ const userAgent = process.env.npm_config_user_agent;
81
+ if (userAgent) {
82
+ if (userAgent.includes("pnpm")) return "pnpm";
83
+ if (userAgent.includes("yarn")) return "yarn";
84
+ if (userAgent.includes("bun")) return "bun";
85
+ }
86
+ return "pnpm";
87
+ }
77
88
  async function installDependencies(cwd, pm) {
78
89
  await execa(pm, ["install"], { cwd, stdio: "inherit" });
79
90
  }
@@ -187,13 +198,24 @@ async function promptBackendConfig() {
187
198
  });
188
199
  includeSwagger = swagger;
189
200
  }
201
+ let includeAuth = false;
202
+ if (backendType === "web") {
203
+ const { auth } = await enquirer2.prompt({
204
+ type: "confirm",
205
+ name: "auth",
206
+ message: "Include JWT authentication?",
207
+ initial: false
208
+ });
209
+ includeAuth = auth;
210
+ }
190
211
  return {
191
212
  type: backendType,
192
213
  framework,
193
214
  database: database === "none" ? void 0 : database,
194
215
  includeRedis,
195
216
  includeSmtp,
196
- includeSwagger
217
+ includeSwagger,
218
+ includeAuth
197
219
  };
198
220
  }
199
221
 
@@ -230,6 +252,7 @@ async function promptFrontendConfig() {
230
252
 
231
253
  // src/generators/monorepo.generator.ts
232
254
  import path2 from "path";
255
+ import { fileURLToPath } from "url";
233
256
 
234
257
  // src/generators/base.generator.ts
235
258
  var BaseGenerator = class {
@@ -254,11 +277,19 @@ var BaseGenerator = class {
254
277
  };
255
278
 
256
279
  // src/generators/monorepo.generator.ts
280
+ var __dirname = path2.dirname(fileURLToPath(import.meta.url));
281
+ var getPackageRoot = () => {
282
+ const dirname = __dirname;
283
+ return path2.resolve(dirname, "..");
284
+ };
257
285
  var MonorepoGenerator = class extends BaseGenerator {
258
286
  async generate(config) {
259
287
  await ensureDir(this.options.projectDir);
260
288
  await this.createRootFiles(config);
261
289
  await this.createPackagesDirectory();
290
+ if (config.includeDocker) {
291
+ await this.createDockerCompose(config);
292
+ }
262
293
  }
263
294
  async createRootFiles(config) {
264
295
  const workspaceContent = `packages:
@@ -393,11 +424,34 @@ ${config.includeBackend ? "\u2502 \u251C\u2500\u2500 api/ # Backend AP
393
424
  async createPackagesDirectory() {
394
425
  await ensureDir(path2.join(this.options.projectDir, "packages"));
395
426
  }
427
+ async createDockerCompose(config) {
428
+ const packageRoot = getPackageRoot();
429
+ const templatePath = path2.join(packageRoot, "templates", "docker-compose.yml");
430
+ const template = await readFile(templatePath);
431
+ const hasPostgres = config.backend?.database === "postgres";
432
+ const hasMongodb = config.backend?.database === "mongodb";
433
+ const hasRedis = config.backend?.includeRedis || false;
434
+ const hasSmtp = config.backend?.includeSmtp || false;
435
+ let content = template.split("\n").filter((line) => {
436
+ if (line.includes("__IF_POSTGRES__")) return hasPostgres;
437
+ if (line.includes("__IF_MONGODB__")) return hasMongodb;
438
+ if (line.includes("__IF_REDIS__")) return hasRedis;
439
+ if (line.includes("__IF_SMTP__")) return hasSmtp;
440
+ return true;
441
+ }).map((line) => {
442
+ return line.replace(/__IF_POSTGRES__/g, "").replace(/__IF_MONGODB__/g, "").replace(/__IF_REDIS__/g, "").replace(/__IF_SMTP__/g, "");
443
+ }).join("\n");
444
+ const tokens = this.getTokens();
445
+ for (const [token, value] of Object.entries(tokens)) {
446
+ content = content.replace(new RegExp(token, "g"), value);
447
+ }
448
+ await writeFile(path2.join(this.options.projectDir, "docker-compose.yml"), content);
449
+ }
396
450
  };
397
451
 
398
452
  // src/generators/backend.generator.ts
399
453
  import path4 from "path";
400
- import { fileURLToPath } from "url";
454
+ import { fileURLToPath as fileURLToPath2 } from "url";
401
455
 
402
456
  // src/utils/template-merger.ts
403
457
  import fs2 from "fs-extra";
@@ -459,9 +513,9 @@ async function mergeEnvFile(baseDir, additionsPath) {
459
513
  }
460
514
 
461
515
  // src/generators/backend.generator.ts
462
- var __dirname = path4.dirname(fileURLToPath(import.meta.url));
463
- var getPackageRoot = () => {
464
- const dirname = __dirname;
516
+ var __dirname2 = path4.dirname(fileURLToPath2(import.meta.url));
517
+ var getPackageRoot2 = () => {
518
+ const dirname = __dirname2;
465
519
  return path4.resolve(dirname, "..");
466
520
  };
467
521
  var BackendGenerator = class extends BaseGenerator {
@@ -479,7 +533,7 @@ var BackendGenerator = class extends BaseGenerator {
479
533
  __HAS_REDIS__: String(this.config.includeRedis),
480
534
  __HAS_SMTP__: String(this.config.includeSmtp)
481
535
  });
482
- const packageRoot = getPackageRoot();
536
+ const packageRoot = getPackageRoot2();
483
537
  let templatePath;
484
538
  if (this.config.type === "web") {
485
539
  templatePath = path4.join(packageRoot, "templates", `backend-${this.config.framework}`, "base");
@@ -507,16 +561,20 @@ var BackendGenerator = class extends BaseGenerator {
507
561
  const swaggerFeaturePath = path4.join(featuresPath, "swagger");
508
562
  await mergeFeature(backendDir, swaggerFeaturePath, tokens);
509
563
  }
564
+ if (this.config.includeAuth) {
565
+ const authFeaturePath = path4.join(featuresPath, "jwt-auth");
566
+ await mergeFeature(backendDir, authFeaturePath, tokens);
567
+ }
510
568
  }
511
569
  }
512
570
  };
513
571
 
514
572
  // src/generators/frontend.generator.ts
515
573
  import path5 from "path";
516
- import { fileURLToPath as fileURLToPath2 } from "url";
517
- var __dirname2 = path5.dirname(fileURLToPath2(import.meta.url));
518
- var getPackageRoot2 = () => {
519
- const dirname = __dirname2;
574
+ import { fileURLToPath as fileURLToPath3 } from "url";
575
+ var __dirname3 = path5.dirname(fileURLToPath3(import.meta.url));
576
+ var getPackageRoot3 = () => {
577
+ const dirname = __dirname3;
520
578
  return path5.resolve(dirname, "..");
521
579
  };
522
580
  var FrontendGenerator = class extends BaseGenerator {
@@ -531,7 +589,7 @@ var FrontendGenerator = class extends BaseGenerator {
531
589
  const tokens = this.getTokens({
532
590
  __API_URL__: this.config.apiUrl
533
591
  });
534
- const packageRoot = getPackageRoot2();
592
+ const packageRoot = getPackageRoot3();
535
593
  const templatePath = path5.join(packageRoot, "templates", "frontend-nextjs", "base");
536
594
  await copyDirRecursive(templatePath, frontendDir, tokens);
537
595
  }
@@ -539,10 +597,10 @@ var FrontendGenerator = class extends BaseGenerator {
539
597
 
540
598
  // src/generators/shared.generator.ts
541
599
  import path6 from "path";
542
- import { fileURLToPath as fileURLToPath3 } from "url";
543
- var __dirname3 = path6.dirname(fileURLToPath3(import.meta.url));
544
- var getPackageRoot3 = () => {
545
- const dirname = __dirname3;
600
+ import { fileURLToPath as fileURLToPath4 } from "url";
601
+ var __dirname4 = path6.dirname(fileURLToPath4(import.meta.url));
602
+ var getPackageRoot4 = () => {
603
+ const dirname = __dirname4;
546
604
  return path6.resolve(dirname, "..");
547
605
  };
548
606
  var SharedGenerator = class extends BaseGenerator {
@@ -550,57 +608,151 @@ var SharedGenerator = class extends BaseGenerator {
550
608
  const sharedDir = path6.join(this.options.projectDir, "packages", "shared");
551
609
  await ensureDir(sharedDir);
552
610
  const tokens = this.getTokens();
553
- const packageRoot = getPackageRoot3();
611
+ const packageRoot = getPackageRoot4();
554
612
  const templatePath = path6.join(packageRoot, "templates", "shared", "base");
555
613
  await copyDirRecursive(templatePath, sharedDir, tokens);
556
614
  }
557
615
  };
558
616
 
559
617
  // src/commands/create.ts
560
- async function createMonorepo(targetDir) {
618
+ function showPreview(config) {
561
619
  console.log("");
562
- logger.info("Welcome to create-monorepo!");
620
+ console.log(chalk.bold("\u{1F4E6} Project Preview:"));
563
621
  console.log("");
564
- const projectName = await promptProjectName(targetDir ? path7.basename(targetDir) : void 0);
565
- const projectDir = path7.resolve(process.cwd(), targetDir || projectName);
566
- if (!await isDirEmpty(projectDir)) {
567
- logger.error(`Directory "${projectDir}" is not empty. Please choose an empty directory.`);
568
- process.exit(1);
622
+ console.log(chalk.cyan(`${config.projectName}/`));
623
+ console.log(chalk.gray("\u251C\u2500\u2500 packages/"));
624
+ if (config.includeBackend && config.backend) {
625
+ const features = [];
626
+ if (config.backend.database) features.push(config.backend.database.toUpperCase());
627
+ if (config.backend.includeRedis) features.push("Redis");
628
+ if (config.backend.includeSmtp) features.push("SMTP");
629
+ if (config.backend.includeSwagger) features.push("Swagger");
630
+ if (config.backend.includeAuth) features.push("JWT Auth");
631
+ const featureStr = features.length > 0 ? chalk.gray(` (${features.join(", ")})`) : "";
632
+ console.log(chalk.gray(`\u2502 \u251C\u2500\u2500 api/`) + chalk.yellow(` ${config.backend.framework}`) + featureStr);
569
633
  }
570
- const packageManager = await promptPackageManager();
571
- const includeBackend = await promptIncludeBackend();
572
- const includeFrontend = await promptIncludeFrontend();
573
- if (!includeBackend && !includeFrontend) {
574
- logger.error("You must include at least one package (backend or frontend).");
575
- process.exit(1);
634
+ if (config.includeFrontend && config.frontend) {
635
+ const features = [];
636
+ if (config.frontend.includeShadcn) features.push("shadcn/ui");
637
+ const featureStr = features.length > 0 ? chalk.gray(` (${features.join(", ")})`) : "";
638
+ console.log(chalk.gray(`\u2502 \u251C\u2500\u2500 web/`) + chalk.yellow(` Next.js 15`) + featureStr);
576
639
  }
577
- const config = {
578
- projectName,
579
- packageManager,
580
- includeBackend,
581
- includeFrontend
582
- };
583
- if (includeBackend) {
584
- console.log("");
585
- logger.info("Configure backend package:");
586
- config.backend = await promptBackendConfig();
640
+ if (config.includeBackend && config.includeFrontend) {
641
+ console.log(chalk.gray(`\u2502 \u2514\u2500\u2500 shared/`) + chalk.yellow(` Zod schemas + types`));
587
642
  }
588
- if (includeFrontend) {
589
- console.log("");
590
- logger.info("Configure frontend package:");
591
- config.frontend = await promptFrontendConfig();
643
+ console.log(chalk.gray("\u251C\u2500\u2500 pnpm-workspace.yaml"));
644
+ console.log(chalk.gray("\u251C\u2500\u2500 package.json"));
645
+ if (config.includeDocker) {
646
+ console.log(chalk.gray("\u251C\u2500\u2500 docker-compose.yml") + chalk.yellow(" \u26A1"));
592
647
  }
648
+ console.log(chalk.gray("\u2514\u2500\u2500 ..."));
649
+ console.log("");
650
+ }
651
+ async function confirmPreview() {
652
+ const { confirm } = await enquirer4.prompt({
653
+ type: "confirm",
654
+ name: "confirm",
655
+ message: "Does this look good?",
656
+ initial: true
657
+ });
658
+ return confirm;
659
+ }
660
+ async function createMonorepo(targetDir, options = {}) {
593
661
  console.log("");
594
- logger.info("Generating project...");
662
+ logger.info("Welcome to create-monorepo!");
663
+ console.log("");
664
+ const isInteractive = !options.yes && process.stdin.isTTY;
595
665
  try {
666
+ const projectName = isInteractive ? await promptProjectName(targetDir ? path7.basename(targetDir) : void 0) : targetDir ? path7.basename(targetDir) : "my-monorepo";
667
+ const projectDir = path7.resolve(process.cwd(), targetDir || projectName);
668
+ if (!await isDirEmpty(projectDir)) {
669
+ throw new Error(`Directory "${projectDir}" is not empty. Please choose an empty directory.`);
670
+ }
671
+ let packageManager;
672
+ if (options.pm) {
673
+ packageManager = options.pm;
674
+ } else if (isInteractive) {
675
+ packageManager = await promptPackageManager();
676
+ } else {
677
+ packageManager = await detectPackageManager();
678
+ }
679
+ let includeBackend;
680
+ let includeFrontend;
681
+ if (options.yes) {
682
+ includeBackend = options.backend !== void 0 || options.database !== void 0;
683
+ includeFrontend = options.frontend === true;
684
+ if (!includeBackend && !includeFrontend) {
685
+ includeBackend = true;
686
+ includeFrontend = true;
687
+ }
688
+ } else if (isInteractive) {
689
+ includeBackend = await promptIncludeBackend();
690
+ includeFrontend = await promptIncludeFrontend();
691
+ } else {
692
+ includeBackend = true;
693
+ includeFrontend = true;
694
+ }
695
+ if (!includeBackend && !includeFrontend) {
696
+ throw new Error("You must include at least one package (backend or frontend).");
697
+ }
698
+ const config = {
699
+ projectName,
700
+ packageManager,
701
+ includeBackend,
702
+ includeFrontend,
703
+ includeDocker: options.docker
704
+ };
705
+ if (includeBackend) {
706
+ if (options.yes || !isInteractive) {
707
+ config.backend = {
708
+ type: "web",
709
+ framework: options.backend || "hono",
710
+ database: options.database,
711
+ includeRedis: options.redis || false,
712
+ includeSmtp: options.smtp || false,
713
+ includeSwagger: options.swagger || false,
714
+ includeAuth: options.auth || false
715
+ };
716
+ } else {
717
+ console.log("");
718
+ logger.info("Configure backend package:");
719
+ config.backend = await promptBackendConfig();
720
+ }
721
+ }
722
+ if (includeFrontend) {
723
+ if (options.yes || !isInteractive) {
724
+ config.frontend = {
725
+ framework: "nextjs",
726
+ includeShadcn: options.shadcn || false,
727
+ apiUrl: "http://localhost:3001"
728
+ };
729
+ } else {
730
+ console.log("");
731
+ logger.info("Configure frontend package:");
732
+ config.frontend = await promptFrontendConfig();
733
+ }
734
+ }
735
+ if (isInteractive) {
736
+ showPreview(config);
737
+ const confirmed = await confirmPreview();
738
+ if (!confirmed) {
739
+ console.log("");
740
+ logger.info("Cancelled. No files were created.");
741
+ return;
742
+ }
743
+ }
744
+ console.log("");
745
+ logger.info("Generating project...");
746
+ console.log("");
747
+ const spinner = ora();
748
+ spinner.start("Creating monorepo structure...");
596
749
  const monorepoGenerator = new MonorepoGenerator({
597
750
  projectName,
598
751
  projectDir,
599
752
  packageManager
600
753
  });
601
- const spinner = ora("Creating monorepo structure...").start();
602
754
  await monorepoGenerator.generate(config);
603
- spinner.succeed("Monorepo structure created");
755
+ spinner.succeed(chalk.green("Monorepo structure created"));
604
756
  if (includeBackend && includeFrontend) {
605
757
  spinner.start("Generating shared package...");
606
758
  const sharedGenerator = new SharedGenerator({
@@ -609,7 +761,7 @@ async function createMonorepo(targetDir) {
609
761
  packageManager
610
762
  });
611
763
  await sharedGenerator.generate();
612
- spinner.succeed("Shared package generated");
764
+ spinner.succeed(chalk.green("Shared package generated"));
613
765
  }
614
766
  if (includeBackend && config.backend) {
615
767
  spinner.start("Generating backend package...");
@@ -620,7 +772,7 @@ async function createMonorepo(targetDir) {
620
772
  config: config.backend
621
773
  });
622
774
  await backendGenerator.generate();
623
- spinner.succeed("Backend package generated");
775
+ spinner.succeed(chalk.green(`Backend package generated (${config.backend.framework})`));
624
776
  }
625
777
  if (includeFrontend && config.frontend) {
626
778
  spinner.start("Generating frontend package...");
@@ -631,34 +783,61 @@ async function createMonorepo(targetDir) {
631
783
  config: config.frontend
632
784
  });
633
785
  await frontendGenerator.generate();
634
- spinner.succeed("Frontend package generated");
786
+ spinner.succeed(chalk.green("Frontend package generated (Next.js 15)"));
787
+ }
788
+ if (!options.skipInstall) {
789
+ spinner.start(`Installing dependencies with ${packageManager}...`);
790
+ spinner.text = `Installing dependencies... ${chalk.gray("(this may take a minute)")}`;
791
+ await installDependencies(projectDir, packageManager);
792
+ spinner.succeed(chalk.green("Dependencies installed"));
793
+ } else {
794
+ logger.info(chalk.yellow("\u26A0 Skipped dependency installation"));
795
+ }
796
+ if (!options.skipGit) {
797
+ spinner.start("Initializing git repository...");
798
+ await execa2("git", ["init"], { cwd: projectDir });
799
+ await execa2("git", ["add", "."], { cwd: projectDir });
800
+ await execa2("git", ["commit", "-m", "Initial commit from create-monorepo"], { cwd: projectDir });
801
+ spinner.succeed(chalk.green("Git repository initialized"));
802
+ } else {
803
+ logger.info(chalk.yellow("\u26A0 Skipped git initialization"));
635
804
  }
636
- spinner.start("Installing dependencies...");
637
- await installDependencies(projectDir, packageManager);
638
- spinner.succeed("Dependencies installed");
639
- spinner.start("Initializing git repository...");
640
- await execa2("git", ["init"], { cwd: projectDir });
641
- await execa2("git", ["add", "."], { cwd: projectDir });
642
- await execa2("git", ["commit", "-m", "Initial commit from create-monorepo"], { cwd: projectDir });
643
- spinner.succeed("Git repository initialized");
644
805
  console.log("");
645
- logger.success(`Project "${projectName}" created successfully!`);
806
+ logger.success(`${chalk.bold('\u2728 Project "')}${chalk.bold.cyan(projectName)}${chalk.bold('" created successfully!')}`);
646
807
  console.log("");
647
- logger.info("Next steps:");
648
- console.log(` cd ${targetDir || projectName}`);
649
- console.log(` ${packageManager} dev`);
808
+ logger.info(chalk.bold("Next steps:"));
809
+ console.log(chalk.gray(" $ ") + chalk.cyan(`cd ${targetDir || projectName}`));
810
+ if (options.skipInstall) {
811
+ console.log(chalk.gray(" $ ") + chalk.cyan(`${packageManager} install`));
812
+ }
813
+ console.log(chalk.gray(" $ ") + chalk.cyan(`${packageManager} dev`));
650
814
  console.log("");
815
+ if (config.includeDocker) {
816
+ logger.info(chalk.bold("\u{1F433} Docker Compose:"));
817
+ console.log(chalk.gray(" $ ") + chalk.cyan("docker-compose up -d"));
818
+ console.log("");
819
+ }
651
820
  } catch (error) {
652
- logger.error(`Failed to create project: ${error instanceof Error ? error.message : String(error)}`);
821
+ console.log("");
822
+ if (error instanceof Error) {
823
+ logger.error(chalk.red(`\u2716 ${error.message}`));
824
+ if (error.stack && process.env.DEBUG) {
825
+ console.log("");
826
+ console.log(chalk.gray(error.stack));
827
+ }
828
+ } else {
829
+ logger.error(chalk.red(`\u2716 Failed to create project: ${String(error)}`));
830
+ }
831
+ console.log("");
653
832
  process.exit(1);
654
833
  }
655
834
  }
656
835
 
657
836
  // src/index.ts
658
837
  var cli = cac("create-monorepo");
659
- cli.command("[dir]", "Create a brand new monorepo project").action(async (dir) => {
660
- await createMonorepo(dir);
838
+ cli.command("[dir]", "Create a new monorepo project").option("--backend <framework>", "Backend framework (hono)").option("--database <db>", "Database (postgres, mongodb, none)").option("--redis", "Include Redis cache").option("--smtp", "Include SMTP email").option("--swagger", "Include Swagger docs").option("--auth", "Include JWT authentication").option("--frontend", "Include Next.js frontend").option("--shadcn", "Include shadcn/ui components").option("--pm <manager>", "Package manager (pnpm, npm, yarn, bun)").option("--docker", "Include Docker Compose setup").option("--skip-install", "Skip dependency installation").option("--skip-git", "Skip git initialization").option("-y, --yes", "Skip all prompts and use defaults").action(async (dir, options) => {
839
+ await createMonorepo(dir, options);
661
840
  });
662
841
  cli.help();
663
- cli.version("0.1.0");
842
+ cli.version("1.2.0");
664
843
  cli.parse();
package/package.json CHANGED
@@ -1,24 +1,11 @@
1
1
  {
2
2
  "name": "@yoms/create-monorepo",
3
- "version": "1.1.0",
3
+ "version": "2.0.0",
4
4
  "description": "CLI tool to scaffold monorepo projects from templates",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "create-monorepo": "./dist/index.js"
8
8
  },
9
- "scripts": {
10
- "dev": "tsx src/index.ts",
11
- "build": "tsup src/index.ts --format esm --clean",
12
- "typecheck": "tsc --noEmit",
13
- "lint": "eslint src --ext .ts",
14
- "format": "prettier --write \"src/**/*.ts\"",
15
- "test:generate": "tsx scripts/test-generate.ts",
16
- "test:validate": "tsx scripts/validate-templates.ts",
17
- "changeset": "changeset",
18
- "version": "changeset version",
19
- "release": "pnpm build && changeset publish",
20
- "prepublishOnly": "pnpm build && pnpm typecheck"
21
- },
22
9
  "keywords": [
23
10
  "monorepo",
24
11
  "template",
@@ -66,9 +53,25 @@
66
53
  "prettier": "^3.4.2",
67
54
  "tsup": "^8.3.5",
68
55
  "tsx": "^4.19.2",
69
- "typescript": "^5.7.2"
56
+ "typescript": "^5.7.2",
57
+ "vitest": "^2.1.8"
70
58
  },
71
59
  "engines": {
72
60
  "node": ">=18.0.0"
61
+ },
62
+ "scripts": {
63
+ "dev": "tsx src/index.ts",
64
+ "build": "tsup src/index.ts --format esm --clean",
65
+ "typecheck": "tsc --noEmit",
66
+ "lint": "eslint src --ext .ts",
67
+ "format": "prettier --write \"src/**/*.ts\"",
68
+ "test": "vitest run",
69
+ "test:watch": "vitest",
70
+ "test:e2e": "vitest run tests/e2e",
71
+ "test:generate": "tsx scripts/test-generate.ts",
72
+ "test:validate": "tsx scripts/validate-templates.ts",
73
+ "changeset": "changeset",
74
+ "version": "changeset version && pnpm install --lockfile-only",
75
+ "release": "pnpm build && changeset publish"
73
76
  }
74
- }
77
+ }
@@ -0,0 +1,326 @@
1
+ # AGENT.md - Backend API
2
+
3
+ This file provides guidance to AI agents (Claude, Cursor, etc.) when working with this backend API.
4
+
5
+ ## Project Overview
6
+
7
+ This is a Hono-based backend API in a monorepo workspace. It's designed for high performance, full type safety, and production-ready patterns.
8
+
9
+ ## Tech Stack
10
+
11
+ - **Framework**: Hono (lightweight, fast, type-safe)
12
+ - **Language**: TypeScript with strict mode
13
+ - **Runtime**: Node.js (compatible with Bun, Deno, Cloudflare Workers)
14
+ - **Validation**: Zod schemas
15
+ - **Logger**: Winston with structured logging
16
+ - **Build Tool**: tsup (fast ESM bundler)
17
+ - **Testing**: Vitest with coverage
18
+
19
+ ## Development Commands
20
+
21
+ ```bash
22
+ # Development
23
+ pnpm dev # Start with hot reload
24
+ pnpm build # Build for production
25
+ pnpm start # Run production build
26
+ pnpm test # Run tests
27
+ pnpm test:watch # Watch mode
28
+ pnpm test:coverage # Coverage report
29
+ pnpm typecheck # Type checking
30
+ ```
31
+
32
+ ## Project Structure
33
+
34
+ ```
35
+ src/
36
+ ├── config/ # Configuration (env, logger, db)
37
+ ├── lib/ # Utilities and helpers
38
+ │ ├── errors.ts # Custom error classes
39
+ │ └── response.ts # Response helpers
40
+ ├── middleware/ # Hono middleware
41
+ │ ├── cors.middleware.ts
42
+ │ ├── error.middleware.ts
43
+ │ ├── logger.middleware.ts
44
+ │ └── rate-limit.middleware.ts
45
+ ├── routes/ # API route handlers
46
+ │ └── health.route.ts
47
+ ├── services/ # Business logic (if database enabled)
48
+ ├── types/ # TypeScript types
49
+ └── index.ts # App entry point
50
+ ```
51
+
52
+ ## Architecture Patterns
53
+
54
+ ### Error Handling
55
+
56
+ Use custom error classes for consistent error responses:
57
+
58
+ ```typescript
59
+ import { NotFoundError, BadRequestError } from '../lib/errors.js';
60
+
61
+ // Throw custom errors - they're automatically handled
62
+ throw new NotFoundError('User not found');
63
+ throw new BadRequestError('Invalid email format');
64
+ ```
65
+
66
+ Available errors:
67
+ - `BadRequestError` (400)
68
+ - `UnauthorizedError` (401)
69
+ - `ForbiddenError` (403)
70
+ - `NotFoundError` (404)
71
+ - `ConflictError` (409)
72
+ - `ValidationError` (422)
73
+ - `TooManyRequestsError` (429)
74
+ - `InternalServerError` (500)
75
+
76
+ ### Response Helpers
77
+
78
+ Use standardized response helpers:
79
+
80
+ ```typescript
81
+ import { success, created, noContent, paginated } from '../lib/response.js';
82
+
83
+ // Success response (200)
84
+ return success(c, { id: '123', name: 'John' });
85
+
86
+ // Created response (201)
87
+ return created(c, newUser);
88
+
89
+ // No content (204)
90
+ return noContent(c);
91
+
92
+ // Paginated response
93
+ return paginated(c, users, page, limit, total);
94
+ ```
95
+
96
+ ### Route Structure
97
+
98
+ Follow this pattern for new routes:
99
+
100
+ ```typescript
101
+ import { Hono } from 'hono';
102
+ import { zValidator } from '@hono/zod-validator';
103
+ import { success } from '../lib/response.js';
104
+ import { CreateItemSchema } from '@__PROJECT_NAME__/shared';
105
+
106
+ const items = new Hono();
107
+
108
+ // List items with pagination
109
+ items.get('/', zValidator('query', paginationSchema), async (c) => {
110
+ const { page, limit } = c.req.valid('query');
111
+ const result = await ItemService.getAll(page, limit);
112
+ return paginated(c, result.items, page, limit, result.total);
113
+ });
114
+
115
+ // Get by ID
116
+ items.get('/:id', async (c) => {
117
+ const id = c.req.param('id');
118
+ const item = await ItemService.getById(id);
119
+ return success(c, item);
120
+ });
121
+
122
+ // Create
123
+ items.post('/', zValidator('json', CreateItemSchema), async (c) => {
124
+ const data = c.req.valid('json');
125
+ const item = await ItemService.create(data);
126
+ return created(c, item);
127
+ });
128
+
129
+ export { items };
130
+ ```
131
+
132
+ ### Service Layer Pattern
133
+
134
+ Create services for business logic:
135
+
136
+ ```typescript
137
+ import { prisma } from '../config/database.js';
138
+ import { NotFoundError, ConflictError } from '../lib/errors.js';
139
+
140
+ export class ItemService {
141
+ static async getAll(page = 1, limit = 10) {
142
+ const skip = (page - 1) * limit;
143
+ const [items, total] = await Promise.all([
144
+ prisma.item.findMany({ skip, take: limit }),
145
+ prisma.item.count(),
146
+ ]);
147
+ return { items, total, page, limit };
148
+ }
149
+
150
+ static async getById(id: string) {
151
+ const item = await prisma.item.findUnique({ where: { id } });
152
+ if (!item) throw new NotFoundError(`Item ${id} not found`);
153
+ return item;
154
+ }
155
+
156
+ static async create(data: CreateItem) {
157
+ // Check for duplicates
158
+ const existing = await prisma.item.findUnique({ where: { name: data.name } });
159
+ if (existing) throw new ConflictError('Item already exists');
160
+
161
+ return prisma.item.create({ data });
162
+ }
163
+ }
164
+ ```
165
+
166
+ ### Environment Variables
167
+
168
+ All env vars are validated with Zod in `src/config/env.ts`. Add new variables:
169
+
170
+ ```typescript
171
+ const envSchema = z.object({
172
+ NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
173
+ PORT: z.coerce.number().default(3001),
174
+ // Add your vars here
175
+ NEW_VAR: z.string().min(1),
176
+ });
177
+ ```
178
+
179
+ ### Rate Limiting
180
+
181
+ Apply rate limiting to routes:
182
+
183
+ ```typescript
184
+ import { rateLimits } from '../middleware/rate-limit.middleware.js';
185
+
186
+ // Use preset limits
187
+ app.use('/api/*', rateLimits.standard); // 100 req/min
188
+
189
+ // Custom limit
190
+ app.use('/auth/login', rateLimit({
191
+ windowMs: 15 * 60 * 1000, // 15 minutes
192
+ max: 5,
193
+ message: 'Too many login attempts',
194
+ }));
195
+ ```
196
+
197
+ ### Testing
198
+
199
+ Write tests for all routes and services:
200
+
201
+ ```typescript
202
+ import { describe, it, expect } from 'vitest';
203
+ import { Hono } from 'hono';
204
+ import { items } from '../items.route.js';
205
+
206
+ describe('Items Route', () => {
207
+ const app = new Hono();
208
+ app.route('/items', items);
209
+
210
+ it('should list items', async () => {
211
+ const res = await app.request('/items?page=1&limit=10');
212
+ expect(res.status).toBe(200);
213
+
214
+ const json = await res.json();
215
+ expect(json.success).toBe(true);
216
+ expect(json.data).toBeInstanceOf(Array);
217
+ expect(json.pagination).toBeDefined();
218
+ });
219
+ });
220
+ ```
221
+
222
+ ## Database (if enabled)
223
+
224
+ ### Prisma Commands
225
+
226
+ ```bash
227
+ pnpm prisma:generate # Generate Prisma client
228
+ pnpm prisma:migrate # Create and run migrations
229
+ pnpm prisma:studio # Open Prisma Studio UI
230
+ pnpm prisma:push # Push schema to database
231
+ ```
232
+
233
+ ### Creating Migrations
234
+
235
+ ```bash
236
+ # After editing schema.prisma
237
+ pnpm prisma:migrate
238
+ # Enter migration name when prompted
239
+ ```
240
+
241
+ ## Type Sharing
242
+
243
+ Import shared types from the shared package:
244
+
245
+ ```typescript
246
+ import { User, CreateUser, UpdateUser } from '@__PROJECT_NAME__/shared';
247
+ import { UserSchema } from '@__PROJECT_NAME__/shared';
248
+
249
+ // Use schemas for validation
250
+ const result = UserSchema.parse(data);
251
+
252
+ // Use types for type safety
253
+ const user: User = await getUser(id);
254
+ ```
255
+
256
+ ## Best Practices
257
+
258
+ ### DO:
259
+ - ✅ Use custom error classes, not raw `throw new Error()`
260
+ - ✅ Use response helpers for all routes
261
+ - ✅ Validate all input with Zod schemas
262
+ - ✅ Put business logic in services, not routes
263
+ - ✅ Write tests for new features
264
+ - ✅ Use type-safe imports from shared package
265
+ - ✅ Log important operations with structured logging
266
+ - ✅ Handle async operations properly with try/catch
267
+
268
+ ### DON'T:
269
+ - ❌ Don't return raw `c.json()` - use response helpers
270
+ - ❌ Don't put business logic in routes - use services
271
+ - ❌ Don't use `any` type - maintain type safety
272
+ - ❌ Don't skip validation - always validate input
273
+ - ❌ Don't commit `.env` files - use `.env.example`
274
+ - ❌ Don't use blocking operations in async handlers
275
+ - ❌ Don't modify shared types here - edit in `packages/shared`
276
+
277
+ ## Common Tasks
278
+
279
+ ### Adding a New Route
280
+
281
+ 1. Create route file in `src/routes/`
282
+ 2. Create service in `src/services/` (if needed)
283
+ 3. Add validation schemas in `packages/shared/src/schemas/`
284
+ 4. Register route in `src/index.ts`
285
+ 5. Write tests in `src/routes/__tests__/`
286
+
287
+ ### Adding Middleware
288
+
289
+ 1. Create in `src/middleware/`
290
+ 2. Export middleware function
291
+ 3. Apply in `src/index.ts` with `app.use()`
292
+
293
+ ### Adding Environment Variables
294
+
295
+ 1. Add to `.env.example`
296
+ 2. Add to `src/config/env.ts` schema
297
+ 3. Use via `env.YOUR_VAR`
298
+
299
+ ## Debugging
300
+
301
+ ```typescript
302
+ // Use structured logging
303
+ import { logger } from './config/logger.js';
304
+
305
+ logger.info('User logged in', { userId: user.id });
306
+ logger.warn('Rate limit approaching', { ip, count });
307
+ logger.error('Database error', { error, query });
308
+ ```
309
+
310
+ ## Performance Tips
311
+
312
+ - Use database indexes for frequently queried fields
313
+ - Use pagination for list endpoints
314
+ - Cache expensive operations with Redis (if enabled)
315
+ - Use `prisma.$transaction()` for multiple related operations
316
+ - Profile with `pnpm test:coverage` to find slow tests
317
+
318
+ ## Security Checklist
319
+
320
+ - [ ] All inputs are validated with Zod
321
+ - [ ] Rate limiting is applied to auth endpoints
322
+ - [ ] Sensitive data is not logged
323
+ - [ ] Database queries use parameterized queries (Prisma does this)
324
+ - [ ] CORS is configured correctly
325
+ - [ ] Environment variables are validated on startup
326
+ - [ ] Error messages don't leak sensitive info in production
@@ -0,0 +1,5 @@
1
+ # JWT Configuration
2
+ JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
3
+ JWT_EXPIRES_IN=15m
4
+ JWT_REFRESH_SECRET=your-super-secret-refresh-key-change-this-in-production
5
+ JWT_REFRESH_EXPIRES_IN=7d
@@ -0,0 +1,10 @@
1
+ {
2
+ "dependencies": {
3
+ "jsonwebtoken": "^9.0.2",
4
+ "bcryptjs": "^2.4.3"
5
+ },
6
+ "devDependencies": {
7
+ "@types/jsonwebtoken": "^9.0.7",
8
+ "@types/bcryptjs": "^2.4.6"
9
+ }
10
+ }
@@ -0,0 +1,16 @@
1
+ // Add these to your env schema validation
2
+
3
+ import { z } from 'zod';
4
+
5
+ export const jwtEnvSchema = z.object({
6
+ JWT_SECRET: z.string().min(32, 'JWT_SECRET must be at least 32 characters'),
7
+ JWT_EXPIRES_IN: z.string().default('15m'),
8
+ JWT_REFRESH_SECRET: z.string().min(32, 'JWT_REFRESH_SECRET must be at least 32 characters'),
9
+ JWT_REFRESH_EXPIRES_IN: z.string().default('7d'),
10
+ });
11
+
12
+ // Merge this with your existing env schema:
13
+ // const envSchema = z.object({
14
+ // ...existingFields,
15
+ // ...jwtEnvSchema.shape,
16
+ // });
@@ -0,0 +1,75 @@
1
+ import jwt from 'jsonwebtoken';
2
+ import { env } from '../config/env.js';
3
+ import { UnauthorizedError } from '../lib/errors.js';
4
+
5
+ export interface JwtPayload {
6
+ userId: string;
7
+ email: string;
8
+ type: 'access' | 'refresh';
9
+ }
10
+
11
+ export interface TokenPair {
12
+ accessToken: string;
13
+ refreshToken: string;
14
+ }
15
+
16
+ /**
17
+ * Generate access and refresh tokens for a user
18
+ */
19
+ export function generateTokens(userId: string, email: string): TokenPair {
20
+ const accessToken = jwt.sign(
21
+ { userId, email, type: 'access' } as JwtPayload,
22
+ env.JWT_SECRET,
23
+ { expiresIn: env.JWT_EXPIRES_IN }
24
+ );
25
+
26
+ const refreshToken = jwt.sign(
27
+ { userId, email, type: 'refresh' } as JwtPayload,
28
+ env.JWT_REFRESH_SECRET,
29
+ { expiresIn: env.JWT_REFRESH_EXPIRES_IN }
30
+ );
31
+
32
+ return { accessToken, refreshToken };
33
+ }
34
+
35
+ /**
36
+ * Verify and decode an access token
37
+ */
38
+ export function verifyAccessToken(token: string): JwtPayload {
39
+ try {
40
+ const decoded = jwt.verify(token, env.JWT_SECRET) as JwtPayload;
41
+ if (decoded.type !== 'access') {
42
+ throw new UnauthorizedError('Invalid token type');
43
+ }
44
+ return decoded;
45
+ } catch (error) {
46
+ if (error instanceof jwt.TokenExpiredError) {
47
+ throw new UnauthorizedError('Token expired');
48
+ }
49
+ if (error instanceof jwt.JsonWebTokenError) {
50
+ throw new UnauthorizedError('Invalid token');
51
+ }
52
+ throw error;
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Verify and decode a refresh token
58
+ */
59
+ export function verifyRefreshToken(token: string): JwtPayload {
60
+ try {
61
+ const decoded = jwt.verify(token, env.JWT_REFRESH_SECRET) as JwtPayload;
62
+ if (decoded.type !== 'refresh') {
63
+ throw new UnauthorizedError('Invalid token type');
64
+ }
65
+ return decoded;
66
+ } catch (error) {
67
+ if (error instanceof jwt.TokenExpiredError) {
68
+ throw new UnauthorizedError('Refresh token expired');
69
+ }
70
+ if (error instanceof jwt.JsonWebTokenError) {
71
+ throw new UnauthorizedError('Invalid refresh token');
72
+ }
73
+ throw error;
74
+ }
75
+ }
@@ -0,0 +1,50 @@
1
+ import type { Context, Next } from 'hono';
2
+ import { verifyAccessToken } from '../lib/jwt.js';
3
+ import { UnauthorizedError } from '../lib/errors.js';
4
+
5
+ /**
6
+ * Middleware to authenticate requests using JWT
7
+ * Extracts and verifies the JWT token from the Authorization header
8
+ * and attaches the decoded payload to c.get('jwtPayload')
9
+ */
10
+ export async function authMiddleware(c: Context, next: Next): Promise<void> {
11
+ const authHeader = c.req.header('Authorization');
12
+
13
+ if (!authHeader) {
14
+ throw new UnauthorizedError('No authorization header');
15
+ }
16
+
17
+ const [type, token] = authHeader.split(' ');
18
+
19
+ if (type !== 'Bearer' || !token) {
20
+ throw new UnauthorizedError('Invalid authorization header format');
21
+ }
22
+
23
+ const payload = verifyAccessToken(token);
24
+ c.set('jwtPayload', payload);
25
+
26
+ await next();
27
+ }
28
+
29
+ /**
30
+ * Optional authentication middleware
31
+ * Similar to authMiddleware but doesn't throw if no token is present
32
+ * Use this for routes that work for both authenticated and unauthenticated users
33
+ */
34
+ export async function optionalAuthMiddleware(c: Context, next: Next): Promise<void> {
35
+ const authHeader = c.req.header('Authorization');
36
+
37
+ if (authHeader) {
38
+ try {
39
+ const [type, token] = authHeader.split(' ');
40
+ if (type === 'Bearer' && token) {
41
+ const payload = verifyAccessToken(token);
42
+ c.set('jwtPayload', payload);
43
+ }
44
+ } catch {
45
+ // Silently ignore invalid tokens for optional auth
46
+ }
47
+ }
48
+
49
+ await next();
50
+ }
@@ -0,0 +1,157 @@
1
+ import { Hono } from 'hono';
2
+ import { zValidator } from '@hono/zod-validator';
3
+ import { z } from 'zod';
4
+ import bcrypt from 'bcryptjs';
5
+ import { generateTokens, verifyRefreshToken } from '../lib/jwt.js';
6
+ import { success, created } from '../lib/response.js';
7
+ import { UnauthorizedError, ConflictError } from '../lib/errors.js';
8
+
9
+ // NOTE: This is a basic example. In a real application, you would:
10
+ // 1. Store users in a database
11
+ // 2. Store refresh tokens in a database or Redis for invalidation
12
+ // 3. Add email verification
13
+ // 4. Add password reset functionality
14
+ // 5. Add rate limiting on auth endpoints
15
+ // 6. Add account lockout after failed attempts
16
+
17
+ const auth = new Hono();
18
+
19
+ // Validation schemas
20
+ const registerSchema = z.object({
21
+ email: z.string().email(),
22
+ password: z.string().min(8),
23
+ name: z.string().min(1),
24
+ });
25
+
26
+ const loginSchema = z.object({
27
+ email: z.string().email(),
28
+ password: z.string(),
29
+ });
30
+
31
+ const refreshSchema = z.object({
32
+ refreshToken: z.string(),
33
+ });
34
+
35
+ // In-memory user store (replace with database in production)
36
+ interface User {
37
+ id: string;
38
+ email: string;
39
+ password: string;
40
+ name: string;
41
+ createdAt: Date;
42
+ }
43
+
44
+ const users: Map<string, User> = new Map();
45
+
46
+ /**
47
+ * POST /auth/register
48
+ * Register a new user
49
+ */
50
+ auth.post('/register', zValidator('json', registerSchema), async (c) => {
51
+ const { email, password, name } = c.req.valid('json');
52
+
53
+ // Check if user already exists
54
+ const existingUser = Array.from(users.values()).find((u) => u.email === email);
55
+ if (existingUser) {
56
+ throw new ConflictError('User with this email already exists');
57
+ }
58
+
59
+ // Hash password
60
+ const hashedPassword = await bcrypt.hash(password, 10);
61
+
62
+ // Create user
63
+ const userId = crypto.randomUUID();
64
+ const user: User = {
65
+ id: userId,
66
+ email,
67
+ password: hashedPassword,
68
+ name,
69
+ createdAt: new Date(),
70
+ };
71
+
72
+ users.set(userId, user);
73
+
74
+ // Generate tokens
75
+ const tokens = generateTokens(userId, email);
76
+
77
+ return created(c, {
78
+ user: {
79
+ id: user.id,
80
+ email: user.email,
81
+ name: user.name,
82
+ createdAt: user.createdAt,
83
+ },
84
+ ...tokens,
85
+ });
86
+ });
87
+
88
+ /**
89
+ * POST /auth/login
90
+ * Login with email and password
91
+ */
92
+ auth.post('/login', zValidator('json', loginSchema), async (c) => {
93
+ const { email, password } = c.req.valid('json');
94
+
95
+ // Find user
96
+ const user = Array.from(users.values()).find((u) => u.email === email);
97
+ if (!user) {
98
+ throw new UnauthorizedError('Invalid email or password');
99
+ }
100
+
101
+ // Verify password
102
+ const isValidPassword = await bcrypt.compare(password, user.password);
103
+ if (!isValidPassword) {
104
+ throw new UnauthorizedError('Invalid email or password');
105
+ }
106
+
107
+ // Generate tokens
108
+ const tokens = generateTokens(user.id, user.email);
109
+
110
+ return success(c, {
111
+ user: {
112
+ id: user.id,
113
+ email: user.email,
114
+ name: user.name,
115
+ createdAt: user.createdAt,
116
+ },
117
+ ...tokens,
118
+ });
119
+ });
120
+
121
+ /**
122
+ * POST /auth/refresh
123
+ * Refresh access token using refresh token
124
+ */
125
+ auth.post('/refresh', zValidator('json', refreshSchema), async (c) => {
126
+ const { refreshToken } = c.req.valid('json');
127
+
128
+ // Verify refresh token
129
+ const payload = verifyRefreshToken(refreshToken);
130
+
131
+ // Get user
132
+ const user = users.get(payload.userId);
133
+ if (!user) {
134
+ throw new UnauthorizedError('User not found');
135
+ }
136
+
137
+ // Generate new tokens
138
+ const tokens = generateTokens(user.id, user.email);
139
+
140
+ return success(c, tokens);
141
+ });
142
+
143
+ /**
144
+ * POST /auth/logout
145
+ * Logout (client should discard tokens)
146
+ * In a real app, you would invalidate the refresh token in the database
147
+ */
148
+ auth.post('/logout', async (c) => {
149
+ // In a real application, you would:
150
+ // 1. Get refresh token from request
151
+ // 2. Delete it from database/Redis
152
+ // 3. Optionally blacklist the access token until it expires
153
+
154
+ return success(c, { message: 'Logged out successfully' });
155
+ });
156
+
157
+ export default auth;
@@ -0,0 +1,70 @@
1
+ version: '3.8'
2
+
3
+ services:
4
+ __IF_POSTGRES__ postgres:
5
+ __IF_POSTGRES__ image: postgres:16-alpine
6
+ __IF_POSTGRES__ container_name: __PROJECT_NAME__-postgres
7
+ __IF_POSTGRES__ environment:
8
+ __IF_POSTGRES__ POSTGRES_USER: postgres
9
+ __IF_POSTGRES__ POSTGRES_PASSWORD: postgres
10
+ __IF_POSTGRES__ POSTGRES_DB: __PROJECT_NAME__
11
+ __IF_POSTGRES__ ports:
12
+ __IF_POSTGRES__ - '5432:5432'
13
+ __IF_POSTGRES__ volumes:
14
+ __IF_POSTGRES__ - postgres_data:/var/lib/postgresql/data
15
+ __IF_POSTGRES__ healthcheck:
16
+ __IF_POSTGRES__ test: ['CMD-SHELL', 'pg_isready -U postgres']
17
+ __IF_POSTGRES__ interval: 10s
18
+ __IF_POSTGRES__ timeout: 5s
19
+ __IF_POSTGRES__ retries: 5
20
+ __IF_POSTGRES__
21
+ __IF_MONGODB__ mongodb:
22
+ __IF_MONGODB__ image: mongo:7-jammy
23
+ __IF_MONGODB__ container_name: __PROJECT_NAME__-mongodb
24
+ __IF_MONGODB__ environment:
25
+ __IF_MONGODB__ MONGO_INITDB_ROOT_USERNAME: root
26
+ __IF_MONGODB__ MONGO_INITDB_ROOT_PASSWORD: password
27
+ __IF_MONGODB__ MONGO_INITDB_DATABASE: __PROJECT_NAME__
28
+ __IF_MONGODB__ ports:
29
+ __IF_MONGODB__ - '27017:27017'
30
+ __IF_MONGODB__ volumes:
31
+ __IF_MONGODB__ - mongodb_data:/data/db
32
+ __IF_MONGODB__ healthcheck:
33
+ __IF_MONGODB__ test: echo 'db.runCommand("ping").ok' | mongosh localhost:27017/__PROJECT_NAME__ --quiet
34
+ __IF_MONGODB__ interval: 10s
35
+ __IF_MONGODB__ timeout: 5s
36
+ __IF_MONGODB__ retries: 5
37
+ __IF_MONGODB__
38
+ __IF_REDIS__ redis:
39
+ __IF_REDIS__ image: redis:7-alpine
40
+ __IF_REDIS__ container_name: __PROJECT_NAME__-redis
41
+ __IF_REDIS__ ports:
42
+ __IF_REDIS__ - '6379:6379'
43
+ __IF_REDIS__ volumes:
44
+ __IF_REDIS__ - redis_data:/data
45
+ __IF_REDIS__ healthcheck:
46
+ __IF_REDIS__ test: ['CMD', 'redis-cli', 'ping']
47
+ __IF_REDIS__ interval: 10s
48
+ __IF_REDIS__ timeout: 5s
49
+ __IF_REDIS__ retries: 5
50
+ __IF_REDIS__
51
+ __IF_SMTP__ mailhog:
52
+ __IF_SMTP__ image: mailhog/mailhog:latest
53
+ __IF_SMTP__ container_name: __PROJECT_NAME__-mailhog
54
+ __IF_SMTP__ ports:
55
+ __IF_SMTP__ - '1025:1025' # SMTP
56
+ __IF_SMTP__ - '8025:8025' # Web UI
57
+ __IF_SMTP__ healthcheck:
58
+ __IF_SMTP__ test: ['CMD', 'wget', '--quiet', '--tries=1', '--spider', 'http://localhost:8025']
59
+ __IF_SMTP__ interval: 10s
60
+ __IF_SMTP__ timeout: 5s
61
+ __IF_SMTP__ retries: 5
62
+
63
+ volumes:
64
+ __IF_POSTGRES__ postgres_data:
65
+ __IF_MONGODB__ mongodb_data:
66
+ __IF_REDIS__ redis_data:
67
+
68
+ networks:
69
+ default:
70
+ name: __PROJECT_NAME__-network