@kreciko/setup-templates 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.
package/README.md ADDED
@@ -0,0 +1,109 @@
1
+ # setup-templates
2
+
3
+ ๐Ÿš€ Zero-config CLI for quickly adding standard project files to an existing repository.
4
+
5
+ > Perfect for use with **npx** - no installation, no configuration, no pain.
6
+
7
+ ## โœจ What is it?
8
+
9
+ `setup-templates` is a CLI tool that adds ready-made, proven configuration templates to your project in seconds.
10
+ Instead of copying files between repositories - run one command and choose what you want to install.
11
+
12
+ ## ๐Ÿงฐ What's included?
13
+
14
+ - **Express** - TypeScript configuration for Express.js
15
+ - **Frontend** - React + Vite + TypeScript (development & build only)
16
+ - **Docker** - `Dockerfile` and `docker-compose` for development and production
17
+ - **Nginx** - production server for frontend build and API reverse proxy
18
+ - **Base** - MIT license, Biome config and .gitignore
19
+
20
+ ## โ–ถ๏ธ Quick start
21
+
22
+ ```bash
23
+ npx @kretostan/setup-templates
24
+ ```
25
+
26
+ That's it. The CLI will launch an interactive menu where you select which templates to add to your project.
27
+
28
+ ### ๐Ÿ” Specific version (reproducible builds)
29
+
30
+ ```bash
31
+ npx @kretostan/setup-templates@1.2.3
32
+ ```
33
+
34
+ ### ๐Ÿง‘โ€๐Ÿ’ป Local usage (teams / CI)
35
+
36
+ ```bash
37
+ npm install --save-dev @kretostan/setup-templates
38
+ npx @kretostan/setup-templates
39
+ ```
40
+
41
+ ## ๐Ÿ“ฆ What can you install?
42
+
43
+ You can safely install multiple templates in the same project.
44
+ Depending on the selected templates, setup-templates will add the following files and directories to your project.
45
+ Existing files are not overwritten without confirmation.
46
+
47
+ ## ๐Ÿ“‚ What files will be created?
48
+
49
+ ### ๐Ÿงฑ Base
50
+ ```
51
+ โ”œโ”€ .gitignore
52
+ โ”œโ”€ biome.json
53
+ โ””โ”€ LICENSE
54
+ ```
55
+
56
+ ### ๐Ÿณ Docker
57
+ ```
58
+ โ”œโ”€ Dockerfile.backend
59
+ โ”œโ”€ Dockerfile.frontend
60
+ โ”œโ”€ .dockerignore
61
+ โ”œโ”€ Makefile
62
+ โ”œโ”€ compose.dev.yaml
63
+ โ””โ”€ compose.yaml
64
+ ```
65
+
66
+ ### ๐Ÿ–ฅ Express.js
67
+ ```
68
+ โ””โ”€ tsconfig.json
69
+ ```
70
+
71
+ > Express template provides a minimal TypeScript setup, intended to be extended by the user.
72
+
73
+ ### ๐ŸŒ Frontend (React + Vite)
74
+ ```
75
+ โ”œโ”€ tsconfig.app.json
76
+ โ”œโ”€ tsconfig.json
77
+ โ”œโ”€ tsconfig.node.json
78
+ โ””โ”€ vite.config.ts
79
+ ```
80
+
81
+ ### ๐ŸŒ Nginx
82
+ ```
83
+ โ””โ”€ nginx.conf
84
+ ```
85
+
86
+ ## ๐Ÿ’ก Why setup-templates?
87
+
88
+ - โšก works instantly via npx
89
+ - ๐Ÿ“ฆ no manual file copying
90
+ - ๐Ÿงญ interactive template selection
91
+ - ๐Ÿงฉ easy to extend with new presets
92
+ - ๐Ÿงผ consistent project structure across your team
93
+
94
+ ## โš™๏ธ Requirements
95
+
96
+ - Node.js >= 24
97
+ - npm
98
+
99
+ > (Node 24+ required due to modern Node APIs)
100
+
101
+
102
+ ## ๐Ÿ› ๏ธ Libraries used
103
+
104
+ - chalk - colored terminal output
105
+ - ora - spinners
106
+ - prompts - interactive menu
107
+
108
+ ## ๐Ÿ“„ License
109
+ MIT
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { main } from "../src/index.mjs";
3
+
4
+ main(process.argv).catch(error => {
5
+ console.error(error);
6
+ process.exit(1);
7
+ });
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@kreciko/setup-templates",
3
+ "version": "0.1.0",
4
+ "description": "Installer that adds project template files into an existing repo",
5
+ "type": "module",
6
+ "bin": {
7
+ "setup-templates": "bin/setup-templates.mjs"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "templates",
12
+ "src"
13
+ ],
14
+ "engines": {
15
+ "node": ">=24"
16
+ },
17
+ "license": "MIT",
18
+ "dependencies": {
19
+ "chalk": "^5.6.2",
20
+ "ora": "^9.1.0",
21
+ "prompts": "^2.4.2"
22
+ }
23
+ }
package/src/config.mjs ADDED
@@ -0,0 +1,7 @@
1
+ import path from "node:path";
2
+ import {fileURLToPath} from "node:url";
3
+
4
+ const __filename = fileURLToPath(import.meta.url);
5
+ const __dirname = path.dirname(__filename);
6
+
7
+ export const TEMPLATES_DIR = path.join(__dirname, "..", "templates");
package/src/index.mjs ADDED
@@ -0,0 +1,113 @@
1
+ import path from "node:path";
2
+ import ora from "ora";
3
+ import prompts from "prompts";
4
+ import chalk from "chalk";
5
+ import { copyDir, ensureProjectFolder } from "./utils/fs.mjs";
6
+ import { TEMPLATES_DIR } from "./config.mjs";
7
+ import { setTimeout as sleep } from "node:timers/promises";
8
+
9
+ function safeCwd() {
10
+ try {
11
+ return process.cwd();
12
+ } catch {
13
+ const fallback = process.env.INIT_CWD || process.env.PWD || process.env.HOME || "/";
14
+ process.chdir(fallback);
15
+ return process.cwd();
16
+ }
17
+ }
18
+
19
+ export async function main(argv) {
20
+ const cwd = process.cwd();
21
+ ensureProjectFolder(cwd);
22
+
23
+ const args = new Set(argv.slice(2));
24
+ const hasFlags = [...args].some(arg => arg.startsWith("--"));
25
+ const selections = {
26
+ base: args.has("--base"),
27
+ docker: args.has("--docker"),
28
+ nginx: args.has("--nginx"),
29
+ express: args.has("--express"),
30
+ frontend: args.has("--frontend"),
31
+ force: args.has("--force"),
32
+ };
33
+
34
+ const spinner = ora();
35
+ if (!hasFlags) {
36
+ const response = await prompts([
37
+ {
38
+ type: "multiselect",
39
+ name: "features",
40
+ message: " What config would you like to include?",
41
+ choices: [
42
+ { title: chalk.white("Base"), value: "base" },
43
+ { title: chalk.blueBright("Docker (Frontend + Backend)"), value: "docker" },
44
+ { title: chalk.green("Nginx"), value: "nginx" },
45
+ { title: chalk.yellowBright("Express.js (ESM + TS)"), value: "express"},
46
+ { title: chalk.magentaBright("React + Vite + TS"), value: "frontend" },
47
+ ],
48
+ hint: "Space to select. Return to submit.",
49
+ instructions: false,
50
+ min: 1,
51
+ },
52
+ {
53
+ type: "confirm",
54
+ name: "force",
55
+ message: " Do you want to overwrite existing files?",
56
+ initial: false,
57
+ },
58
+ ], {
59
+ onCancel: () => {
60
+ spinner.fail(chalk.redBright(" Canceled"));
61
+ process.exit(0);
62
+ }
63
+ });
64
+
65
+ const set = new Set(response.features ?? []);
66
+ selections.base = set.has("base");
67
+ selections.docker = set.has("docker");
68
+ selections.nginx = set.has("nginx");
69
+ selections.express = set.has("express");
70
+ selections.frontend = set.has("frontend");
71
+ selections.force = !!response.force;
72
+ }
73
+
74
+ spinner.start(chalk.whiteBright("Copying files"));
75
+ await sleep(1000);
76
+ let copied = 0;
77
+
78
+ let warning = "";
79
+ let info = "";
80
+ const rootCheck = new Set(["base", "docker", "nginx",]);
81
+ const frontCheck = new Set(["base", "frontend"]);
82
+ const backCheck = new Set(["base", "express"]);
83
+ Object.keys(selections).forEach(option => {
84
+ if (!selections.force) warning = " Skipping files due to disabled force option.";
85
+ if (!selections[option]) return;
86
+ info = ensureProjectFolder(cwd);
87
+ switch (info) {
88
+ case "root":
89
+ rootCheck.has(option)
90
+ ? copied += copyDir(path.join(TEMPLATES_DIR, option), cwd, selections.force)
91
+ : info = " Some options are unavailable on this level.";
92
+ break;
93
+ case "frontend":
94
+ frontCheck.has(option)
95
+ ? copied += copyDir(path.join(TEMPLATES_DIR, option), cwd, selections.force)
96
+ : info = " Some options are unavailable on this level.";
97
+ break;
98
+ case "backend":
99
+ backCheck.has(option)
100
+ ? copied += copyDir(path.join(TEMPLATES_DIR, option), cwd, selections.force)
101
+ : info = " Some options are unavailable on this level.";
102
+ break;
103
+ default:
104
+ console.log("Error while validating folder.");
105
+ }
106
+ });
107
+
108
+ if (warning.length > 0) spinner.warn(chalk.yellow(warning));
109
+ if (info.length > 0) spinner.info(chalk.blueBright(info));
110
+
111
+ spinner.info(chalk.whiteBright(` Files copied: ${copied}`));
112
+ spinner.succeed(chalk.whiteBright(" All configuration information you can find in README.md"));
113
+ }
@@ -0,0 +1,56 @@
1
+ import fs from "fs";
2
+ import path from "node:path";
3
+ import ora from "ora";
4
+ import chalk from "chalk";
5
+
6
+ function exists(path) {
7
+ try { fs.accessSync(path); return true; } catch { return false; }
8
+ }
9
+
10
+ export function ensureProjectFolder(cwd) {
11
+ const spinner = ora();
12
+ if (!exists(path.join(cwd, "package.json"))) {
13
+ spinner.fail(chalk.red(" Package.json is missing."));
14
+ process.exit(1);
15
+ }
16
+ const pkgPath = path.join(cwd, "package.json");
17
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
18
+
19
+ if (pkg.workspaces && !cwd.endsWith("backend") && !cwd.endsWith("frontend")) return "root";
20
+ if ((pkg.dependencies || pkg.devDependencies) && cwd.endsWith("backend")) return "backend";
21
+ if ((pkg.dependencies || pkg.devDependencies) && cwd.endsWith("frontend")) return "frontend";
22
+
23
+ return "";
24
+ }
25
+
26
+ function listFiles(directory) {
27
+ let out = [];
28
+ for (const file of fs.readdirSync(directory, { withFileTypes: true })) {
29
+ const fullPath = path.join(directory, file.name);
30
+ if (file.isDirectory()) {
31
+ out.push(...listFiles(fullPath));
32
+ } else {
33
+ out.push(fullPath);
34
+ }
35
+ }
36
+ return out;
37
+ }
38
+
39
+ export function copyDir(srcDir, destDir, force = false) {
40
+ const files = listFiles(srcDir);
41
+ let count = 0;
42
+
43
+ for (const srcFile of files) {
44
+ const relative = path.relative(srcDir, srcFile);
45
+ const destFile = path.join(destDir, relative);
46
+ const destDirName = path.dirname(destFile);
47
+
48
+ if (exists(destFile) && !force) {
49
+ continue;
50
+ }
51
+ fs.mkdirSync(destDirName, { recursive: true });
52
+ fs.copyFileSync(srcFile, destFile);
53
+ count++;
54
+ }
55
+ return count;
56
+ }
@@ -0,0 +1,8 @@
1
+ The MIT License (MIT)
2
+ Copyright ยฉ 2025 Jakub Kret
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the โ€œSoftwareโ€), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
5
+
6
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
7
+
8
+ THE SOFTWARE IS PROVIDED โ€œAS ISโ€, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,42 @@
1
+ {
2
+ "$schema": "https://biomejs.dev/schemas/2.3.13/schema.json",
3
+ "vcs": {
4
+ "enabled": true,
5
+ "clientKind": "git",
6
+ "useIgnoreFile": true
7
+ },
8
+ "files": {
9
+ "ignoreUnknown": false
10
+ },
11
+ "formatter": {
12
+ "enabled": true,
13
+ "indentStyle": "tab"
14
+ },
15
+ "css": {
16
+ "parser": {
17
+ "tailwindDirectives": true
18
+ }
19
+ },
20
+ "linter": {
21
+ "enabled": true,
22
+ "rules": {
23
+ "recommended": true,
24
+ "correctness": {
25
+ "useUniqueElementIds": "off"
26
+ }
27
+ }
28
+ },
29
+ "javascript": {
30
+ "formatter": {
31
+ "quoteStyle": "double"
32
+ }
33
+ },
34
+ "assist": {
35
+ "enabled": true,
36
+ "actions": {
37
+ "source": {
38
+ "organizeImports": "on"
39
+ }
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,34 @@
1
+ # Dependencies
2
+ node_modules
3
+ .pnpm-store
4
+ .yarn
5
+
6
+ # Logs
7
+ *.log
8
+ npm-debug.log*
9
+ yarn-error.log*
10
+
11
+ # Build outputs (if you build in Docker)
12
+ dist
13
+ build
14
+ .out
15
+ .next
16
+ .nuxt
17
+ coverage
18
+
19
+ # Env
20
+ .env
21
+ .env.*
22
+ !.env.example
23
+
24
+ # VCS / IDE / OS
25
+ .git
26
+ .gitignore
27
+ .vscode
28
+ .idea
29
+ .DS_Store
30
+
31
+ # temp / cache
32
+ tmp
33
+ temp
34
+ .cache
@@ -0,0 +1,27 @@
1
+ FROM node:alpine AS deps
2
+ WORKDIR /app
3
+ COPY package.json package-lock.json ./
4
+ COPY frontend/package.json ./frontend/package.json
5
+ COPY backend/package.json ./backend/package.json
6
+ RUN npm install
7
+
8
+ FROM deps AS dev
9
+ WORKDIR /app
10
+ COPY backend ./backend
11
+ EXPOSE 3001
12
+ CMD ["npm", "--workspace", "backend", "run", "dev"]
13
+
14
+ FROM deps AS build
15
+ WORKDIR /app
16
+ COPY backend ./backend
17
+ RUN npm --workspace backend run build
18
+
19
+ FROM node:alpine AS prod
20
+ WORKDIR /app
21
+ COPY package.json package-lock.json ./
22
+ COPY frontend/package.json ./frontend/package.json
23
+ COPY backend/package.json ./backend/package.json
24
+ RUN npm ci --omit=dev
25
+ COPY --from=build /app/backend/dist ./backend/dist
26
+ EXPOSE 3000
27
+ CMD ["npm", "--workspace", "backend", "start"]
@@ -0,0 +1,21 @@
1
+ FROM node:alpine AS deps
2
+ WORKDIR /app
3
+ COPY package.json package-lock.json ./
4
+ COPY frontend/package.json ./frontend/package.json
5
+ COPY backend/package.json ./backend/package.json
6
+ RUN npm install
7
+
8
+ FROM deps AS dev
9
+ WORKDIR /app
10
+ COPY frontend ./frontend
11
+ EXPOSE 5173
12
+ CMD ["npm", "--workspace", "frontend", "run", "dev"]
13
+
14
+ FROM deps AS build
15
+ WORKDIR /app
16
+ COPY frontend ./frontend
17
+ RUN npm --workspace frontend run build
18
+
19
+ FROM nginx:alpine AS prod
20
+ COPY --from=build /app/frontend/dist /usr/share/nginx/html
21
+ EXPOSE 80
@@ -0,0 +1,23 @@
1
+ dev-up:
2
+ docker compose -p app-dev -f compose.dev.yaml --env-file .env.development up --build -d
3
+
4
+ dev-start:
5
+ docker compose -p app-dev -f compose.dev.yaml --env-file .env.development up --build -d
6
+
7
+ dev-stop:
8
+ docker compose -p app-dev stop
9
+
10
+ dev-down:
11
+ docker compose -p app-dev down
12
+
13
+ prod-up:
14
+ docker compose -p app-prod -f compose.yaml --env-file .env up --build -d
15
+
16
+ prod-start:
17
+ docker compose -p app-prod -f compose.yaml --env-file .env up --build -d
18
+
19
+ prod-stop:
20
+ docker compose -p app-prod stop
21
+
22
+ prod-down:
23
+ docker compose -p app-prod down
@@ -0,0 +1,41 @@
1
+ services:
2
+ backend:
3
+ build:
4
+ context: .
5
+ target: dev
6
+ dockerfile: Dockerfile.backend
7
+ ports:
8
+ - "${BACKEND_PORT:-3001}:3001"
9
+ env_file:
10
+ - ./.env.development
11
+ environment:
12
+ # - NODE_ENV=${NODE_ENV:-development}
13
+ - PORT=${BACKEND_PORT:-3001}
14
+ volumes:
15
+ - ./backend:/app/backend
16
+ - backend_node_modules:/app/backend/node_modules
17
+ healthcheck:
18
+ test: ["CMD-SHELL", "exit 0"]
19
+ disable: true
20
+
21
+ frontend:
22
+ build:
23
+ context: .
24
+ target: dev
25
+ dockerfile: Dockerfile.frontend
26
+ ports:
27
+ - "${FRONTEND_PORT:-5173}:5173"
28
+ env_file:
29
+ - ./.env.development
30
+ environment:
31
+ - NODE_ENV=${NODE_ENV:-development}
32
+ - CHOKIDAR_USEPOLLING=true
33
+ volumes:
34
+ - ./frontend:/app/frontend
35
+ - frontend_node_modules:/app/frontend/node_modules
36
+ depends_on:
37
+ - backend
38
+
39
+ volumes:
40
+ backend_node_modules:
41
+ frontend_node_modules:
@@ -0,0 +1,31 @@
1
+ services:
2
+ backend:
3
+ build:
4
+ context: .
5
+ target: prod
6
+ dockerfile: Dockerfile.backend
7
+ environment:
8
+ # - NODE_ENV=production
9
+ - PORT=3000
10
+ - APP_URL=http://localhost
11
+ expose:
12
+ - "3000"
13
+ healthcheck:
14
+ test: ["CMD-SHELL", "node -e \"require('http').get('http://localhost:3000/health',res=>process.exit(res.statusCode===200?0:1)).on('error',()=>process.exit(1))\""]
15
+ interval: 5s
16
+ timeout: 2s
17
+ retries: 10
18
+ start_period: 5s
19
+
20
+ frontend:
21
+ build:
22
+ context: .
23
+ target: prod
24
+ dockerfile: Dockerfile.frontend
25
+ ports:
26
+ - "80:80"
27
+ volumes:
28
+ - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
29
+ depends_on:
30
+ backend:
31
+ condition: service_healthy
@@ -0,0 +1,28 @@
1
+ {
2
+ "compilerOptions": {
3
+ "rootDir": "./src",
4
+ "outDir": "./dist",
5
+
6
+ "module": "NodeNext",
7
+ "target": "ESNext",
8
+ "moduleResolution": "nodenext",
9
+
10
+ "sourceMap": true,
11
+ "declaration": true,
12
+ "declarationMap": true,
13
+
14
+ "noUnusedLocals": true,
15
+ "noImplicitReturns": true,
16
+ "esModuleInterop": true,
17
+
18
+ "noUncheckedIndexedAccess": true,
19
+ "exactOptionalPropertyTypes": false,
20
+ "strict": true,
21
+ "types": ["node"],
22
+
23
+ "baseUrl": "../backend",
24
+ "paths": { "@/*": ["src/*"] }
25
+ },
26
+ "compileOnSave": true,
27
+ "include": ["src"]
28
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4
+ "target": "ES2022",
5
+ "useDefineForClassFields": true,
6
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
7
+ "module": "ESNext",
8
+ "types": ["vite/client"],
9
+ "skipLibCheck": true,
10
+
11
+ "moduleResolution": "bundler",
12
+ "allowImportingTsExtensions": true,
13
+ "verbatimModuleSyntax": true,
14
+ "moduleDetection": "force",
15
+ "noEmit": true,
16
+ "jsx": "react-jsx",
17
+
18
+ "strict": true,
19
+ "noUnusedLocals": true,
20
+ "noUnusedParameters": true,
21
+ "erasableSyntaxOnly": true,
22
+ "noFallthroughCasesInSwitch": true,
23
+ "noUncheckedSideEffectImports": true
24
+ },
25
+ "include": ["src"]
26
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ { "path": "./tsconfig.app.json" },
5
+ { "path": "./tsconfig.node.json" }
6
+ ],
7
+ "compilerOptions": {
8
+ "baseUrl": ".",
9
+ "paths": {
10
+ "@/*": ["src/*"]
11
+ }
12
+ }
13
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4
+ "target": "ES2023",
5
+ "lib": ["ES2023"],
6
+ "module": "ESNext",
7
+ "types": ["node"],
8
+ "skipLibCheck": true,
9
+
10
+ "moduleResolution": "bundler",
11
+ "allowImportingTsExtensions": true,
12
+ "verbatimModuleSyntax": true,
13
+ "moduleDetection": "force",
14
+ "noEmit": true,
15
+
16
+ "strict": true,
17
+ "noUnusedLocals": true,
18
+ "noUnusedParameters": true,
19
+ "erasableSyntaxOnly": true,
20
+ "noFallthroughCasesInSwitch": true,
21
+ "noUncheckedSideEffectImports": true
22
+ },
23
+ "include": ["vite.config.ts"]
24
+ }
@@ -0,0 +1,17 @@
1
+ import * as path from "node:path";
2
+ import react from "@vitejs/plugin-react-swc";
3
+ import { defineConfig } from 'vite';
4
+
5
+ // https://vite.dev/config/
6
+ export default defineConfig({
7
+ plugins: [react()],
8
+ server: {
9
+ host: "0.0.0.0",
10
+ port: 5173
11
+ },
12
+ resolve: {
13
+ alias: {
14
+ "@": path.resolve(__dirname, "./src"),
15
+ },
16
+ },
17
+ })
@@ -0,0 +1,67 @@
1
+ server {
2
+ listen 80;
3
+
4
+ # Root directory where the built frontend (HTML/CSS/JS) files are located
5
+ root /usr/share/nginx/html;
6
+
7
+ # Default file to serve
8
+ index index.html;
9
+
10
+ # Gzip compression
11
+ gzip on;
12
+ gzip_comp_level 5; # Compression on level 1โ€“9 (5 = sweet spot)
13
+ gzip_min_length 1024; # Do not compress very small files
14
+ gzip_vary on; # Add header Vary: Accept-Encoding
15
+
16
+ # MIME types
17
+ gzip_types
18
+ text/plain
19
+ text/css
20
+ application/javascript
21
+ application/json
22
+ application/xml
23
+ text/xml
24
+ image/svg+xml;
25
+
26
+ # Cache static assets for a long time (1 year)
27
+ # Good for production builds where filenames include hashes (e.g. main.abc123.js)
28
+ location ~* \.(js|css|png|jpg|jpeg|gif|svg|woff2)$ {
29
+ expires 1y;
30
+ add_header Cache-Control "public, immutable";
31
+ try_files $uri =404;
32
+ }
33
+
34
+ # Main frontend route โ€” needed for SPA frameworks (React, Vue, Svelte, etc.)
35
+ # Any unknown route falls back to index.html
36
+ location / {
37
+ try_files $uri /index.html;
38
+ }
39
+
40
+ # API reverse proxy โ€” forwards /api/* requests to backend service
41
+ # "backend" should match the service name in docker-compose (or another host)
42
+ location ^~ /api/ {
43
+ # Forward requests to the backend service
44
+ # Trailing slash removes /api prefix before passing the request upstream
45
+ proxy_pass http://backend:3000/;
46
+
47
+ # Preserve original request information
48
+ # Allows backend to know the real host, client IP and protocol
49
+ proxy_set_header Host $host;
50
+ proxy_set_header X-Real-IP $remote_addr;
51
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
52
+ proxy_set_header X-Forwarded-Proto $scheme;
53
+
54
+ # Enable WebSocket and other connection upgrades
55
+ proxy_http_version 1.1;
56
+ proxy_set_header Upgrade $http_upgrade;
57
+ proxy_set_header Connection "upgrade";
58
+
59
+ # Prevent hanging connections and slow upstreams
60
+ proxy_connect_timeout 5s;
61
+ proxy_send_timeout 60s;
62
+ proxy_read_timeout 60s;
63
+
64
+ # Maximum allowed request body size (e.g. file uploads)
65
+ client_max_body_size 20m;
66
+ }
67
+ }