@pauldvlp/vp-react-ts-nestjs 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +73 -0
  3. package/dist/index.js +188 -0
  4. package/package.json +46 -0
  5. package/template/README.md +122 -0
  6. package/template/_dockerignore +8 -0
  7. package/template/_gitignore +30 -0
  8. package/template/apps/api/Dockerfile +32 -0
  9. package/template/apps/api/_env.example +3 -0
  10. package/template/apps/api/_gitignore +8 -0
  11. package/template/apps/api/package.json +33 -0
  12. package/template/apps/api/src/app.module.ts +33 -0
  13. package/template/apps/api/src/common/zod-validation.pipe.ts +16 -0
  14. package/template/apps/api/src/config/config.module.ts +11 -0
  15. package/template/apps/api/src/config/env.ts +14 -0
  16. package/template/apps/api/src/health/health.controller.ts +9 -0
  17. package/template/apps/api/src/health/health.module.ts +8 -0
  18. package/template/apps/api/src/items/items.controller.ts +25 -0
  19. package/template/apps/api/src/items/items.module.ts +10 -0
  20. package/template/apps/api/src/items/items.service.spec.ts +26 -0
  21. package/template/apps/api/src/items/items.service.ts +24 -0
  22. package/template/apps/api/src/main.ts +30 -0
  23. package/template/apps/api/tsconfig.json +28 -0
  24. package/template/apps/api/vite.config.ts +35 -0
  25. package/template/apps/web/_gitignore +24 -0
  26. package/template/apps/web/index.html +13 -0
  27. package/template/apps/web/package.json +27 -0
  28. package/template/apps/web/public/favicon.svg +4 -0
  29. package/template/apps/web/src/App.tsx +48 -0
  30. package/template/apps/web/src/main.tsx +9 -0
  31. package/template/apps/web/tsconfig.app.json +31 -0
  32. package/template/apps/web/tsconfig.json +4 -0
  33. package/template/apps/web/tsconfig.node.json +23 -0
  34. package/template/apps/web/vite.config.ts +45 -0
  35. package/template/package.json +26 -0
  36. package/template/packages/contracts/package.json +24 -0
  37. package/template/packages/contracts/src/index.ts +18 -0
  38. package/template/packages/contracts/tsconfig.json +19 -0
  39. package/template/packages/contracts/vite.config.ts +20 -0
  40. package/template/pnpm-workspace.yaml +29 -0
  41. package/template/tsconfig.json +9 -0
  42. package/template/vite.config.ts +24 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 pauldvlp
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,73 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/pauldvlp/vp-templates/main/assets/cover.webp" alt="@pauldvlp/vp-templates" width="100%" />
3
+ </p>
4
+
5
+ # @pauldvlp/vp-react-ts-nestjs
6
+
7
+ A [Vite+](https://viteplus.dev) **monorepo generator** that scaffolds a full-stack workspace where the
8
+ front-end and the back-end share one toolchain:
9
+
10
+ - `apps/web` — a minimal React + Vite+ app that proxies `/api` to the server (no CORS)
11
+ - `apps/api` — a NestJS api conformed to Vite+ (no Webpack, no `nest` CLI)
12
+ - `packages/contracts` — shared **Zod** schemas + inferred types, the single source of truth for both ends
13
+
14
+ The api rides Vite+'s native **Oxc** transform, which emits `emitDecoratorMetadata` from `tsconfig.json`
15
+ — so Nest's dependency injection works with **no transform plugin**. Dev runs on `vite-node --watch`,
16
+ the production build is a Vite SSR bundle (`dist/main.js`), and a single `vp check` / `vp test` /
17
+ `vp run -r build` covers the whole workspace.
18
+
19
+ It's a [Bingo](https://create.bingo) template, so options can be passed on the `vp create` command line
20
+ (anything after `--`).
21
+
22
+ ## Usage
23
+
24
+ Published under the [`@pauldvlp/create`](../create) manifest:
25
+
26
+ ```bash
27
+ # Interactive (prompts for anything you don't pass)
28
+ vp create @pauldvlp:vp-react-ts-nestjs
29
+
30
+ # Non-interactive, fully specified
31
+ vp create @pauldvlp:vp-react-ts-nestjs -- \
32
+ --name my-app --scope @acme --apiPort 3000 --webPort 5173
33
+ ```
34
+
35
+ > Only **string** options parse reliably as `vp create -- --flag value`. **Boolean** options
36
+ > (`--swagger`, `--serveWeb`, `--docker`, `--install`) are best left to the interactive prompt — Bingo's
37
+ > CLI does not accept `--no-x` / `--x=false` cleanly. Omit them and answer the prompt.
38
+
39
+ ## Options
40
+
41
+ | Option | Type / values | Default | Notes |
42
+ | ------------ | ------------- | --------- | ---------------------------------------------------------------------------------------------- |
43
+ | `--name` | string | `my-app` | Root project / package name. |
44
+ | `--scope` | string | `@<name>` | npm scope for workspace packages → `@scope/web`, `@scope/api`, `@scope/contracts`. Defaults to the project name prefixed with `@` (e.g. `--name acme` → `@acme`); falls back to `@app`. |
45
+ | `--apiPort` | string | `3000` | Port the NestJS api listens on (substituted into the env default + the web proxy). |
46
+ | `--webPort` | string | `5173` | Port the web dev server listens on. |
47
+ | `--swagger` | boolean | `false` | Expose Swagger UI at `/docs` and the OpenAPI JSON at `/docs.json` (adds `@nestjs/swagger`). |
48
+ | `--serveWeb` | boolean | `false` | Have the api serve the built web app for a single deployable (adds `@nestjs/serve-static`). |
49
+ | `--docker` | boolean | `false` | Emit a multi-stage `apps/api/Dockerfile` (+ root `.dockerignore`). |
50
+ | `--install` | boolean | `true` | Run `pnpm install` after scaffolding. `false` = files only. |
51
+
52
+ ## What it scaffolds
53
+
54
+ `produce()` reads the static monorepo skeleton under `template/`, rewriting the `@app` scope, project
55
+ name and ports, then conditionally wires the optional features via marker comments:
56
+
57
+ - **`--swagger`** swaps the `// __SWAGGER_*__` markers in `apps/api/src/main.ts` for a `DocumentBuilder` +
58
+ `SwaggerModule.setup('docs', …, { jsonDocumentUrl: 'docs.json' })`; otherwise the markers are stripped.
59
+ - **`--serveWeb`** swaps the `// __SERVEWEB_*__` markers in `apps/api/src/app.module.ts` for a
60
+ `ServeStaticModule.forRoot` pointing at `apps/web/dist` (excluding `/api/*`); otherwise stripped.
61
+ - **`--docker`** keeps `apps/api/Dockerfile` + the root `.dockerignore`; otherwise both are dropped.
62
+
63
+ The optional Nest deps (`@nestjs/swagger`, `@nestjs/serve-static`) are added to `apps/api/package.json`
64
+ only when their flag is on, and the README's `<!-- SWAGGER/SERVEWEB/DOCKER -->` doc blocks are kept or
65
+ dropped to match. With `--install`, `pnpm install` runs as a post-scaffold step.
66
+
67
+ ## Develop the generator
68
+
69
+ ```bash
70
+ pnpm install
71
+ node bin/index.ts --help # list options
72
+ node bin/index.ts --directory /tmp/demo --name demo --scope @demo --apiPort 3000 --webPort 5173
73
+ ```
package/dist/index.js ADDED
@@ -0,0 +1,188 @@
1
+ #!/usr/bin/env node
2
+
3
+ // bin/index.ts
4
+ import { runTemplateCLI } from "bingo";
5
+
6
+ // src/template.ts
7
+ import path2 from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+
10
+ // ../template-kit/src/index.ts
11
+ import fs from "node:fs";
12
+ import path from "node:path";
13
+ var RENAME = {
14
+ _gitignore: ".gitignore",
15
+ _dockerignore: ".dockerignore",
16
+ "_env.example": ".env.example"
17
+ };
18
+ function toScope(name) {
19
+ const n = name.trim();
20
+ return n.startsWith("@") ? n : `@${n}`;
21
+ }
22
+ function readTree(dir, transform, base = dir) {
23
+ const out = {};
24
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
25
+ const abs = path.join(dir, entry.name);
26
+ const name = RENAME[entry.name] ?? entry.name;
27
+ if (entry.isDirectory()) {
28
+ out[name] = readTree(abs, transform, base);
29
+ } else {
30
+ const rel = path.relative(base, abs);
31
+ out[name] = transform(rel, fs.readFileSync(abs, "utf8"));
32
+ }
33
+ }
34
+ return out;
35
+ }
36
+ function patchJson(tree, relPath, mutate) {
37
+ const parts = relPath.split("/");
38
+ let node = tree;
39
+ for (let i = 0; i < parts.length - 1; i++) node = node[parts[i]];
40
+ const key = parts[parts.length - 1];
41
+ const json = JSON.parse(node[key]);
42
+ mutate(json);
43
+ node[key] = `${JSON.stringify(json, null, 2)}
44
+ `;
45
+ }
46
+
47
+ // src/template.ts
48
+ import { createTemplate } from "bingo";
49
+ import { z } from "zod";
50
+
51
+ // package.json
52
+ var package_default = {
53
+ name: "@pauldvlp/vp-react-ts-nestjs",
54
+ version: "0.1.0",
55
+ description: "Vite+ monorepo template: a React web app + a NestJS api (conformed to the Vite+ toolchain) sharing Zod contracts.",
56
+ author: "pauldvlp (https://github.com/pauldvlp/vp-templates)",
57
+ license: "MIT",
58
+ homepage: "https://github.com/pauldvlp/vp-templates",
59
+ repository: {
60
+ type: "git",
61
+ url: "git+https://github.com/pauldvlp/vp-templates.git",
62
+ directory: "packages/vp-react-ts-nestjs"
63
+ },
64
+ bugs: "https://github.com/pauldvlp/vp-templates/issues",
65
+ keywords: [
66
+ "vite-plus-generator",
67
+ "vite-plus",
68
+ "nestjs",
69
+ "react",
70
+ "template"
71
+ ],
72
+ bin: "./dist/index.js",
73
+ type: "module",
74
+ files: [
75
+ "dist",
76
+ "template"
77
+ ],
78
+ scripts: {
79
+ build: "esbuild bin/index.ts --bundle --outfile=dist/index.js --format=esm --platform=node --target=node22 --packages=external",
80
+ dev: "node bin/index.ts",
81
+ prepack: "pnpm run build"
82
+ },
83
+ dependencies: {
84
+ bingo: "^0.9.3",
85
+ zod: "^3.25.76"
86
+ },
87
+ devDependencies: {
88
+ "@pauldvlp/template-kit": "workspace:*",
89
+ "@types/node": "^24",
90
+ esbuild: "^0.25.0",
91
+ typescript: "^5"
92
+ },
93
+ engines: {
94
+ node: ">=22.18.0"
95
+ }
96
+ };
97
+
98
+ // src/template.ts
99
+ var TEMPLATE_DIR = path2.join(path2.dirname(fileURLToPath(import.meta.url)), "..", "template");
100
+ var MAIN_TS = path2.join("apps", "api", "src", "main.ts");
101
+ var SWAGGER_IMPORT = "import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'";
102
+ var APP_MODULE = path2.join("apps", "api", "src", "app.module.ts");
103
+ var SERVEWEB_MODULE = [
104
+ " ServeStaticModule.forRoot({",
105
+ " // Serve the built web app. `import.meta.dirname` resolves the same for src/main.ts (dev) and",
106
+ " // dist/main.js (prod) \u2014 both sit two levels under apps/api, so ../../web/dist is apps/web/dist.",
107
+ " rootPath: join(import.meta.dirname, '..', '..', 'web', 'dist'),",
108
+ " exclude: ['/api/*path']",
109
+ " }),"
110
+ ].join("\n");
111
+ function applyDocBlock(text, tag, keep) {
112
+ return keep ? text.replace(new RegExp(`^<!-- ${tag}:(START|END) -->\\n`, "gm"), "") : text.replace(new RegExp(`<!-- ${tag}:START -->\\n[\\s\\S]*?<!-- ${tag}:END -->\\n`, "g"), "");
113
+ }
114
+ var template_default = createTemplate({
115
+ about: {
116
+ name: package_default.name,
117
+ description: package_default.description
118
+ },
119
+ options: {
120
+ name: z.string().describe("Root project / package name").default("my-app"),
121
+ scope: z.string().describe("npm scope for workspace packages, e.g. @acme (defaults to @<name>)"),
122
+ // Ports are strings, not z.number(): Bingo's clack prompt renders an option's default as a text
123
+ // placeholder and calls `.slice` on it, which throws for non-string defaults. They're only ever
124
+ // substituted into config files as text anyway.
125
+ apiPort: z.string().describe("Port the NestJS api listens on").default("3000"),
126
+ webPort: z.string().describe("Port the web dev server listens on").default("5173"),
127
+ swagger: z.boolean().describe("Expose Swagger UI at /docs on the api").default(false),
128
+ serveWeb: z.boolean().describe("Have the api serve the built web app (single deployable)").default(false),
129
+ docker: z.boolean().describe("Add a multi-stage Dockerfile for the api").default(false),
130
+ install: z.boolean().describe("Install deps after scaffolding").default(true)
131
+ },
132
+ // Lazily default the package scope to `@<name>` (prefixing `@` unless the name already has one),
133
+ // so it tracks the project name instead of a fixed value. Falls back to `@app` when no name is set.
134
+ prepare({ options }) {
135
+ return {
136
+ scope: () => options.name ? toScope(options.name) : "@app"
137
+ };
138
+ },
139
+ async produce({ options }) {
140
+ const scope = toScope(options.scope || options.name || "app");
141
+ const swaggerSetup = [
142
+ `const swaggerConfig = new DocumentBuilder().setTitle('${options.name} API').setVersion('1.0').build()`,
143
+ `SwaggerModule.setup('docs', app, SwaggerModule.createDocument(app, swaggerConfig), { jsonDocumentUrl: 'docs.json' })`
144
+ ].join("\n ");
145
+ const files = readTree(TEMPLATE_DIR, (rel, content) => {
146
+ let out = content.split("@app").join(scope).split("__PROJECT_NAME__").join(options.name).split("__API_PORT__").join(options.apiPort).split("__WEB_PORT__").join(options.webPort);
147
+ if (rel === MAIN_TS) {
148
+ out = options.swagger ? out.replace("// __SWAGGER_IMPORT__", SWAGGER_IMPORT).replace("// __SWAGGER_SETUP__", swaggerSetup) : out.replace(/^[ \t]*\/\/ __SWAGGER_(IMPORT|SETUP)__\n/gm, "");
149
+ }
150
+ if (rel === APP_MODULE) {
151
+ out = options.serveWeb ? out.replace("// __SERVEWEB_PATH_IMPORT__", "import { join } from 'node:path'\n").replace("// __SERVEWEB_MODULE_IMPORT__", "import { ServeStaticModule } from '@nestjs/serve-static'").replace(" // __SERVEWEB_MODULE__", SERVEWEB_MODULE) : out.replace(/^[ \t]*\/\/ __SERVEWEB_[A-Z_]+__\n/gm, "");
152
+ }
153
+ if (rel === "README.md") {
154
+ out = applyDocBlock(out, "SWAGGER", options.swagger);
155
+ out = applyDocBlock(out, "SERVEWEB", options.serveWeb);
156
+ out = applyDocBlock(out, "DOCKER", options.docker);
157
+ }
158
+ return out;
159
+ });
160
+ if (options.swagger || options.serveWeb) {
161
+ patchJson(files, "apps/api/package.json", (pkg) => {
162
+ const deps = { ...pkg.dependencies };
163
+ if (options.swagger) deps["@nestjs/swagger"] = "^11";
164
+ if (options.serveWeb) deps["@nestjs/serve-static"] = "^5";
165
+ pkg.dependencies = Object.fromEntries(Object.entries(deps).sort(([a], [b]) => a.localeCompare(b)));
166
+ });
167
+ }
168
+ if (!options.docker) {
169
+ delete files[".dockerignore"];
170
+ delete files.apps.api["Dockerfile"];
171
+ }
172
+ const scripts = options.install ? [{ commands: ["pnpm install --silent"], phase: 0 }] : [];
173
+ return {
174
+ files,
175
+ scripts,
176
+ suggestions: [
177
+ `cd into the project and run \`vp run -r dev\` to start web + api together.`,
178
+ `The web app proxies \`/api\` to the NestJS server on port ${options.apiPort}.`,
179
+ ...options.swagger ? [`Swagger UI: http://localhost:${options.apiPort}/docs (OpenAPI JSON at /docs.json).`] : [],
180
+ ...options.serveWeb ? [`--serveWeb is on: after \`vp run -r build\`, \`node apps/api/dist/main.js\` serves the web app from the api.`] : [],
181
+ options.install ? `Run \`vp check\` to lint, format and type-check the whole workspace.` : `Skipped install. Run \`vp install\`, then \`vp run -r dev\`.`
182
+ ]
183
+ };
184
+ }
185
+ });
186
+
187
+ // bin/index.ts
188
+ process.exitCode = await runTemplateCLI(template_default);
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@pauldvlp/vp-react-ts-nestjs",
3
+ "version": "0.1.0",
4
+ "description": "Vite+ monorepo template: a React web app + a NestJS api (conformed to the Vite+ toolchain) sharing Zod contracts.",
5
+ "author": "pauldvlp (https://github.com/pauldvlp/vp-templates)",
6
+ "license": "MIT",
7
+ "homepage": "https://github.com/pauldvlp/vp-templates",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/pauldvlp/vp-templates.git",
11
+ "directory": "packages/vp-react-ts-nestjs"
12
+ },
13
+ "bugs": "https://github.com/pauldvlp/vp-templates/issues",
14
+ "keywords": [
15
+ "vite-plus-generator",
16
+ "vite-plus",
17
+ "nestjs",
18
+ "react",
19
+ "template"
20
+ ],
21
+ "type": "module",
22
+ "files": [
23
+ "dist",
24
+ "template"
25
+ ],
26
+ "dependencies": {
27
+ "bingo": "^0.9.3",
28
+ "zod": "^3.25.76"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^24",
32
+ "esbuild": "^0.25.0",
33
+ "typescript": "^5",
34
+ "@pauldvlp/template-kit": "0.0.0"
35
+ },
36
+ "engines": {
37
+ "node": ">=22.18.0"
38
+ },
39
+ "scripts": {
40
+ "build": "esbuild bin/index.ts --bundle --outfile=dist/index.js --format=esm --platform=node --target=node22 --packages=external",
41
+ "dev": "node bin/index.ts"
42
+ },
43
+ "bin": {
44
+ "vp-react-ts-nestjs": "./dist/index.js"
45
+ }
46
+ }
@@ -0,0 +1,122 @@
1
+ # __PROJECT_NAME__
2
+
3
+ Monorepo full-stack basado en [Vite+](https://viteplus.dev): **una app web (React)** + un **api (NestJS)** que comparten **contratos Zod**.
4
+
5
+ ```
6
+ .
7
+ ├── apps/
8
+ │ ├── web # @app/web — React + Vite+, proxea /api al backend
9
+ │ └── api # @app/api — NestJS, conformado a la toolchain de Vite+
10
+ └── packages/
11
+ └── contracts # @app/contracts — schemas Zod + tipos compartidos (única fuente de verdad)
12
+ ```
13
+
14
+ ## Requisitos
15
+
16
+ - Node `>=22.18.0`
17
+ - pnpm `11.9.0` (se descarga solo vía `devEngines`)
18
+ - CLI `vp` (Vite+) instalado globalmente
19
+
20
+ ## Empezar
21
+
22
+ ```bash
23
+ vp install # si aún no se instaló al crear el proyecto
24
+ vp run -r dev # levanta web + api juntos
25
+ ```
26
+
27
+ - Web: <http://localhost:__WEB_PORT__>
28
+ - API: <http://localhost:__API_PORT__/api> (p.ej. `/api/health`, `/api/items`)
29
+
30
+ El front llama a `/api/*` y Vite+ lo **proxea** al api en dev (mismo origen, sin CORS).
31
+
32
+ ## Scripts (raíz)
33
+
34
+ | Script | Qué hace |
35
+ | ----------------- | ------------------------------------------------- |
36
+ | `vp run -r dev` | Levanta web + api a la vez |
37
+ | `vp check` | Formatea, lintea y type-checkea todo el workspace |
38
+ | `vp run -r build` | Buildea todos los paquetes |
39
+ | `vp test` | Corre los tests (vite-plus/test) de todo el workspace |
40
+ | `pnpm ready` | `vp check` + build (pensado para CI) |
41
+
42
+ ## Cómo está cableado el `api` (NestJS sobre Vite+)
43
+
44
+ NestJS necesita `emitDecoratorMetadata` para su inyección de dependencias. El transform **Oxc** de
45
+ vite-plus lo emite de forma nativa (a partir de `experimentalDecorators` + `emitDecoratorMetadata`
46
+ en `tsconfig.json`), así que el `api` es un paquete vite-plus normal, sin plugins de transform:
47
+
48
+ - **`vp run @app/api#dev`** → `vite-node --watch src/main.ts` (reinicio limpio en cada cambio: el
49
+ bloque `import.meta.hot.dispose` en `main.ts` cierra el server antes de re-ejecutar).
50
+ - **`vp build`** → build **SSR** de Vite que externaliza `node_modules` y emite `dist/main.js`
51
+ (arráncalo con `node dist/main.js` o `vp run @app/api#start`).
52
+ - **`vp check` / `vp test`** → incluyen el `api` automáticamente; los specs (`*.spec.ts`) corren por
53
+ el mismo pipeline, así que la DI de Nest funciona en los tests (ver `items.service.spec.ts`).
54
+
55
+ ## Contratos compartidos (`@app/contracts`)
56
+
57
+ Los schemas Zod viven en `packages/contracts` y se importan desde ambos lados:
58
+
59
+ - El **api** valida los bodies con un `ZodValidationPipe` propio y mínimo (`apps/api/src/common`).
60
+ - La **web** tipa sus `fetch` con los tipos inferidos (`Item`, `CreateItem`).
61
+
62
+ ## Configuración y logs
63
+
64
+ - **Env**: `apps/api/src/config/env.ts` valida `process.env` con Zod al arrancar (falla rápido).
65
+ Copia `apps/api/.env.example` a `.env`.
66
+ - **Logs**: [`nestjs-pino`](https://github.com/iamolegga/nestjs-pino). En dev se imprimen legibles
67
+ (vía `pino-pretty`, una línea) en la misma terminal que la web; en producción salen como JSON.
68
+
69
+ <!-- SWAGGER:START -->
70
+ ## Swagger
71
+
72
+ La documentación interactiva (Swagger UI) se sirve en <http://localhost:__API_PORT__/docs> y el
73
+ documento OpenAPI en crudo en `/docs.json`. Con el api levantado (`vp run -r dev`), compruébalo:
74
+
75
+ ```bash
76
+ curl -s -o /dev/null -w "%{http_code}\n" http://localhost:__API_PORT__/docs # 200
77
+ curl -s http://localhost:__API_PORT__/docs.json | head -c 200 # JSON OpenAPI
78
+ ```
79
+
80
+ La configuración vive en `apps/api/src/main.ts` (`DocumentBuilder` + `SwaggerModule.setup`). Para
81
+ que un endpoint aparezca documentado con su esquema, anótalo con los decoradores de `@nestjs/swagger`
82
+ (`@ApiTags`, `@ApiResponse`, …).
83
+
84
+ <!-- SWAGGER:END -->
85
+ <!-- SERVEWEB:START -->
86
+ ## El api sirve el web (single deployable)
87
+
88
+ Con `--serveWeb`, el api monta `apps/web/dist` con `@nestjs/serve-static` (excluyendo `/api/*`), así
89
+ que un solo proceso sirve front + API. Es para **producción** (en dev se usa el proxy). Compruébalo:
90
+
91
+ ```bash
92
+ vp run -r build # genera apps/web/dist y apps/api/dist
93
+ node apps/api/dist/main.js # o: vp run @app/api#start
94
+ # el api sirve el front y la API en el mismo puerto:
95
+ curl -s -o /dev/null -w "%{http_code}\n" http://localhost:__API_PORT__/ # 200 (index.html)
96
+ curl -s http://localhost:__API_PORT__/api/health # {"status":"ok"}
97
+ ```
98
+
99
+ La ruta del estático se resuelve en `apps/api/src/app.module.ts` (`ServeStaticModule.forRoot`).
100
+
101
+ <!-- SERVEWEB:END -->
102
+ <!-- DOCKER:START -->
103
+ ## Docker
104
+
105
+ El `api` trae un `Dockerfile` multi-stage (`apps/api/Dockerfile`). El **contexto de build debe ser la
106
+ raíz del monorepo** (el api se construye desde todo el workspace):
107
+
108
+ ```bash
109
+ docker build -f apps/api/Dockerfile -t __PROJECT_NAME__-api .
110
+ docker run --rm -p __API_PORT__:__API_PORT__ -e PORT=__API_PORT__ __PROJECT_NAME__-api
111
+ # en otra terminal, comprueba:
112
+ curl -s http://localhost:__API_PORT__/api/health # {"status":"ok"}
113
+ ```
114
+
115
+ Es un punto de partida (la imagen incluye el workspace completo). Para una imagen más liviana, parte
116
+ de `pnpm deploy --filter @app/api --prod`.
117
+
118
+ <!-- DOCKER:END -->
119
+ ## Añadir un recurso
120
+
121
+ Mira `apps/api/src/items` (controller + service + module en memoria) y `health` como plantillas, y
122
+ declara su contrato en `packages/contracts`.
@@ -0,0 +1,8 @@
1
+ node_modules
2
+ dist
3
+ **/node_modules
4
+ **/dist
5
+ .git
6
+ *.log
7
+ .env
8
+ .env.*
@@ -0,0 +1,30 @@
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ build
14
+ *.local
15
+
16
+ # Env
17
+ .env
18
+ .env.*
19
+ !.env.example
20
+
21
+ # Editor directories and files
22
+ .vscode/*
23
+ !.vscode/extensions.json
24
+ .idea
25
+ .DS_Store
26
+ *.suo
27
+ *.ntvs*
28
+ *.njsproj
29
+ *.sln
30
+ *.sw?
@@ -0,0 +1,32 @@
1
+ # Multi-stage image for @app/api. The build context must be the MONOREPO ROOT (the api is built from
2
+ # the whole workspace), e.g. from the repo root:
3
+ #
4
+ # docker build -f apps/api/Dockerfile -t app-api .
5
+ #
6
+ # This is a pragmatic starting point — it ships the full installed workspace. For a slimmer image,
7
+ # look at `pnpm deploy --filter @app/api --prod`.
8
+ FROM node:22-slim AS build
9
+ # CI=true: pnpm purges a stale modules dir non-interactively instead of aborting (no TTY in a build).
10
+ # The prompt-disable lets corepack fetch pnpm without asking.
11
+ ENV CI=true
12
+ ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0
13
+ # vite-plus's native core initializes an HTTPS client on startup and panics without system CA certs,
14
+ # which the -slim image omits.
15
+ RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && rm -rf /var/lib/apt/lists/*
16
+ RUN corepack enable
17
+ WORKDIR /repo
18
+ COPY . .
19
+ # --ignore-scripts skips the `prepare` git-hooks lifecycle script (added when you opt into pre-commit
20
+ # hooks at `vp create`), which needs git/a repo and is pointless in an image. The build needs no
21
+ # install scripts.
22
+ RUN pnpm install --frozen-lockfile --ignore-scripts
23
+ # Build the whole workspace so apps/web/dist exists too (needed when the api serves the web app).
24
+ RUN pnpm -r run build
25
+
26
+ FROM node:22-slim AS runtime
27
+ ENV NODE_ENV=production
28
+ WORKDIR /repo
29
+ # The api externalizes its dependencies in the SSR build, so node_modules must be present at runtime.
30
+ COPY --from=build /repo ./
31
+ EXPOSE __API_PORT__
32
+ CMD ["node", "apps/api/dist/main.js"]
@@ -0,0 +1,3 @@
1
+ # Copy to .env and adjust. Validated by src/config/env.ts at boot.
2
+ NODE_ENV=development
3
+ PORT=__API_PORT__
@@ -0,0 +1,8 @@
1
+ node_modules
2
+ dist
3
+ *.log
4
+
5
+ # Env
6
+ .env
7
+ .env.*
8
+ !.env.example
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@app/api",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite-node --watch src/main.ts",
8
+ "build": "vp build",
9
+ "start": "node dist/main.js",
10
+ "lint": "vp lint",
11
+ "check": "vp check"
12
+ },
13
+ "dependencies": {
14
+ "@app/contracts": "workspace:*",
15
+ "@nestjs/common": "^11",
16
+ "@nestjs/core": "^11",
17
+ "@nestjs/platform-express": "^11",
18
+ "nestjs-pino": "^4",
19
+ "pino-http": "^10",
20
+ "reflect-metadata": "^0.2",
21
+ "rxjs": "^7.8",
22
+ "zod": "^3.25.76"
23
+ },
24
+ "devDependencies": {
25
+ "@nestjs/testing": "^11",
26
+ "@types/node": "catalog:",
27
+ "pino-pretty": "^13",
28
+ "typescript": "catalog:",
29
+ "vite": "catalog:",
30
+ "vite-node": "^6",
31
+ "vite-plus": "catalog:"
32
+ }
33
+ }
@@ -0,0 +1,33 @@
1
+ // __SERVEWEB_PATH_IMPORT__
2
+ import { Module } from '@nestjs/common'
3
+ // __SERVEWEB_MODULE_IMPORT__
4
+ import { LoggerModule } from 'nestjs-pino'
5
+
6
+ import { ConfigModule } from './config/config.module'
7
+ import { ENV, type Env } from './config/env'
8
+ import { HealthModule } from './health/health.module'
9
+ import { ItemsModule } from './items/items.module'
10
+
11
+ @Module({
12
+ imports: [
13
+ // __SERVEWEB_MODULE__
14
+ ConfigModule,
15
+ LoggerModule.forRootAsync({
16
+ // Read NODE_ENV from the Zod-validated env (the global ConfigModule) instead of raw process.env.
17
+ inject: [ENV],
18
+ useFactory: (env: Env) => ({
19
+ pinoHttp: {
20
+ // pino emits JSON by default. In dev, pretty-print it (single line, colored) so the logs are
21
+ // readable in the same terminal as the web dev server. In production, emit raw JSON to stdout.
22
+ transport: env.NODE_ENV === 'production' ? undefined : { target: 'pino-pretty', options: { singleLine: true } }
23
+ },
24
+ // Named wildcard (`*path`) instead of the legacy `*`: with `setGlobalPrefix('api')` this middleware
25
+ // route becomes `/api/*path`, which Express 5 / path-to-regexp 8 accepts without a deprecation warning.
26
+ forRoutes: ['*path']
27
+ })
28
+ }),
29
+ HealthModule,
30
+ ItemsModule
31
+ ]
32
+ })
33
+ export class AppModule {}
@@ -0,0 +1,16 @@
1
+ import { BadRequestException, type PipeTransform } from '@nestjs/common'
2
+ import type { ZodSchema } from 'zod'
3
+
4
+ /**
5
+ * Minimal, zero-dependency bridge between Zod and Nest's pipe system. Use it per-route against a
6
+ * schema from `@app/contracts`, e.g. `@Body(new ZodValidationPipe(createItemSchema)) body: CreateItem`.
7
+ */
8
+ export class ZodValidationPipe<T> implements PipeTransform {
9
+ constructor(private readonly schema: ZodSchema<T>) {}
10
+
11
+ transform(value: unknown): T {
12
+ const result = this.schema.safeParse(value)
13
+ if (!result.success) throw new BadRequestException(result.error.flatten())
14
+ return result.data
15
+ }
16
+ }
@@ -0,0 +1,11 @@
1
+ import { Global, Module } from '@nestjs/common'
2
+
3
+ import { ENV, loadEnv } from './env'
4
+
5
+ // Global so any provider can inject the validated env via `@Inject(ENV)`.
6
+ @Global()
7
+ @Module({
8
+ providers: [{ provide: ENV, useFactory: loadEnv }],
9
+ exports: [ENV]
10
+ })
11
+ export class ConfigModule {}
@@ -0,0 +1,14 @@
1
+ import { z } from 'zod'
2
+
3
+ /** Validate `process.env` at boot — the app fails fast with a clear error if something is missing. */
4
+ export const envSchema = z.object({
5
+ NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
6
+ PORT: z.coerce.number().default(__API_PORT__)
7
+ })
8
+
9
+ export type Env = z.infer<typeof envSchema>
10
+
11
+ /** DI token for the parsed, validated environment. */
12
+ export const ENV = Symbol('ENV')
13
+
14
+ export const loadEnv = (): Env => envSchema.parse(process.env)
@@ -0,0 +1,9 @@
1
+ import { Controller, Get } from '@nestjs/common'
2
+
3
+ @Controller('health')
4
+ export class HealthController {
5
+ @Get()
6
+ check(): { status: string } {
7
+ return { status: 'ok' }
8
+ }
9
+ }