create-exts-app 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +117 -0
  3. package/bin/cli.js +212 -0
  4. package/package.json +36 -0
  5. package/templates/advanced/.dockerignore +8 -0
  6. package/templates/advanced/.editorconfig +9 -0
  7. package/templates/advanced/.gitattributes +1 -0
  8. package/templates/advanced/.github/workflows/ci.yml +32 -0
  9. package/templates/advanced/.github/workflows/release.yml +19 -0
  10. package/templates/advanced/.husky/pre-commit +1 -0
  11. package/templates/advanced/.husky/pre-push +1 -0
  12. package/templates/advanced/.vscode/extensions.json +6 -0
  13. package/templates/advanced/.vscode/settings.json +8 -0
  14. package/templates/advanced/Dockerfile +17 -0
  15. package/templates/advanced/docker-compose.yml +12 -0
  16. package/templates/advanced/eslint.config.mjs +35 -0
  17. package/templates/advanced/package.json +56 -0
  18. package/templates/advanced/prettier.config.mjs +10 -0
  19. package/templates/advanced/src/app.ts +20 -0
  20. package/templates/advanced/src/config/env.ts +10 -0
  21. package/templates/advanced/src/config/logger.ts +11 -0
  22. package/templates/advanced/src/config/swagger.ts +39 -0
  23. package/templates/advanced/src/index.ts +8 -0
  24. package/templates/advanced/src/middleware/errorHandler.ts +18 -0
  25. package/templates/advanced/src/middleware/security.ts +17 -0
  26. package/templates/advanced/src/middleware/validate.ts +26 -0
  27. package/templates/advanced/src/routes/health.ts +9 -0
  28. package/templates/advanced/tests/app.test.ts +26 -0
  29. package/templates/advanced/tests/setup.ts +9 -0
  30. package/templates/advanced/tsconfig.json +47 -0
  31. package/templates/advanced/vitest.config.ts +25 -0
  32. package/templates/minimal/package.json +23 -0
  33. package/templates/minimal/src/app.ts +13 -0
  34. package/templates/minimal/src/index.ts +7 -0
  35. package/templates/minimal/tsconfig.json +26 -0
  36. package/templates/standard/.editorconfig +9 -0
  37. package/templates/standard/.husky/pre-commit +1 -0
  38. package/templates/standard/.vscode/extensions.json +6 -0
  39. package/templates/standard/.vscode/settings.json +8 -0
  40. package/templates/standard/eslint.config.mjs +35 -0
  41. package/templates/standard/package.json +46 -0
  42. package/templates/standard/prettier.config.mjs +10 -0
  43. package/templates/standard/src/app.ts +13 -0
  44. package/templates/standard/src/index.ts +7 -0
  45. package/templates/standard/tests/app.test.ts +20 -0
  46. package/templates/standard/tsconfig.json +32 -0
  47. package/templates/standard/vitest.config.ts +14 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
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,117 @@
1
+ # create-exts-app
2
+
3
+ A CLI to scaffold a production-ready **Express + TypeScript** API in seconds โ€” no configuration required.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/create-exts-app.svg)](https://www.npmjs.com/package/create-exts-app)
6
+ [![license](https://img.shields.io/npm/l/create-exts-app.svg)](./LICENSE)
7
+ [![node](https://img.shields.io/node/v/create-exts-app.svg)](https://nodejs.org)
8
+
9
+ ## Quick Start
10
+
11
+ ```bash
12
+ npx create-exts-app my-api
13
+ cd my-api
14
+ npm run dev
15
+ ```
16
+
17
+ That's it โ€” you have a running Express + TypeScript server with hot reload.
18
+
19
+ You can also run it without a project name to get interactive prompts:
20
+
21
+ ```bash
22
+ npx create-exts-app
23
+ ```
24
+
25
+ ## Features
26
+
27
+ - โšก **Three template variants** โ€” pick exactly the amount of tooling you need
28
+ - ๐Ÿงญ **Interactive setup** โ€” guided prompts when you don't pass flags
29
+ - ๐Ÿงน **Built-in tooling** โ€” ESLint, Prettier, Husky, lint-staged, Vitest
30
+ - ๐Ÿณ **Optional Docker support** โ€” Dockerfile + docker-compose on request
31
+ - ๐Ÿ” **Production-ready middleware** (advanced template) โ€” Helmet, CORS, rate limiting, structured logging with Pino, Zod validation, Swagger docs
32
+ - ๐Ÿ”ง **CI/CD included** (advanced template) โ€” GitHub Actions workflows for CI and release
33
+ - ๐Ÿ“ฆ **Modern ESM** โ€” native ES modules, `tsx` for dev, path aliases where applicable
34
+ - ๐Ÿš€ **Zero config** โ€” one command, working project
35
+
36
+ ## Templates
37
+
38
+ | Template | Includes |
39
+ |------------|----------|
40
+ | `minimal` | Express + TypeScript only. Nothing else. |
41
+ | `standard` | + ESLint, Prettier, Husky, lint-staged, Vitest, Supertest |
42
+ | `advanced` | + Docker, Swagger (OpenAPI), Zod validation, Pino logging, Helmet, CORS, rate limiting, GitHub Actions CI/CD |
43
+
44
+ Choose one with `-t`/`--template`, or pick interactively if you omit it.
45
+
46
+ ## Usage
47
+
48
+ ```bash
49
+ npx create-exts-app <project-name> [options]
50
+ ```
51
+
52
+ ### Options
53
+
54
+ | Flag | Description | Default |
55
+ |------|-------------|---------|
56
+ | `-t, --template <type>` | Template to use: `minimal`, `standard`, or `advanced` | `standard` |
57
+ | `--docker` | Include Docker setup (`standard`/`advanced` only) | `false` |
58
+ | `--skip-git` | Skip `git init` | `false` |
59
+ | `--skip-install` | Skip `npm install` after scaffolding | `false` |
60
+ | `-V, --version` | Print the CLI version | โ€” |
61
+ | `-h, --help` | Show help | โ€” |
62
+
63
+ ### Examples
64
+
65
+ ```bash
66
+ # Interactive mode โ€” prompts for name, template, and Docker
67
+ npx create-exts-app
68
+
69
+ # Standard template (default), skip Docker prompt entirely
70
+ npx create-exts-app my-api
71
+
72
+ # Advanced template with Docker included
73
+ npx create-exts-app my-api --template advanced --docker
74
+
75
+ # Minimal template, no git repo, no auto-install
76
+ npx create-exts-app my-api --template minimal --skip-git --skip-install
77
+ ```
78
+
79
+ ## What You Get
80
+
81
+ ```
82
+ my-api/
83
+ โ”œโ”€โ”€ src/
84
+ โ”‚ โ”œโ”€โ”€ index.ts # Entry point
85
+ โ”‚ โ”œโ”€โ”€ app.ts # Express app setup
86
+ โ”‚ โ”œโ”€โ”€ config/ # Env, logger, Swagger (standard/advanced)
87
+ โ”‚ โ”œโ”€โ”€ middleware/ # Security, error handling, validation (advanced)
88
+ โ”‚ โ””โ”€โ”€ routes/ # API routes (advanced)
89
+ โ”œโ”€โ”€ tests/ # Vitest + Supertest (standard/advanced)
90
+ โ”œโ”€โ”€ package.json
91
+ โ”œโ”€โ”€ tsconfig.json
92
+ โ””โ”€โ”€ ...tooling configs (ESLint, Prettier, Husky, Docker, CI)
93
+ ```
94
+
95
+ Once inside your generated project, the standard commands are available:
96
+
97
+ | Command | Description |
98
+ |---------|-------------|
99
+ | `npm run dev` | Start the dev server with hot reload |
100
+ | `npm run build` | Compile TypeScript to `dist/` |
101
+ | `npm start` | Run the compiled production build |
102
+ | `npm test` | Run tests (standard/advanced) |
103
+ | `npm run lint` | Lint the source (standard/advanced) |
104
+ | `npm run format` | Format with Prettier (standard/advanced) |
105
+
106
+ ## Requirements
107
+
108
+ - Node.js `>= 20.0.0`
109
+ - npm (or your package manager of choice, after scaffolding)
110
+
111
+ ## Contributing
112
+
113
+ Issues and pull requests are welcome. If you're proposing a larger change (a new template, a new flag), open an issue first to discuss it.
114
+
115
+ ## License
116
+
117
+ MIT ยฉ [Mohammad Hossein Noughabi](https://www.npmjs.com/mohammadnoughabi)
package/bin/cli.js ADDED
@@ -0,0 +1,212 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { fileURLToPath } from 'node:url';
4
+ import { dirname, join } from 'node:path';
5
+ import { existsSync, readFileSync, writeFileSync, cpSync, mkdirSync, rmSync } from 'node:fs';
6
+ import { execSync } from 'node:child_process';
7
+
8
+ import { Command } from 'commander';
9
+ import inquirer from 'inquirer';
10
+ import chalk from 'chalk';
11
+ import ora from 'ora';
12
+ import figlet from 'figlet';
13
+
14
+ const __dirname = dirname(fileURLToPath(import.meta.url));
15
+ const program = new Command();
16
+
17
+ program
18
+ .name('create-exts-app')
19
+ .description('Scaffold a production-ready Express + TypeScript API')
20
+ .version('1.0.0')
21
+ .argument('[project-name]', 'Name of your project')
22
+ .option('-t, --template <type>', 'Template variant', 'standard')
23
+ .option('--skip-git', 'Skip Git initialization')
24
+ .option('--skip-install', 'Skip dependency installation')
25
+ .option('--docker', 'Include Docker setup')
26
+ .action(async (projectName, options) => {
27
+ // Interactive prompts if name not provided
28
+ if (!projectName) {
29
+ const answers = await inquirer.prompt([
30
+ {
31
+ type: 'input',
32
+ name: 'name',
33
+ message: 'Project name:',
34
+ validate: (input) => input.trim() !== '' || 'Name is required',
35
+ },
36
+ {
37
+ type: 'list',
38
+ name: 'template',
39
+ message: 'Choose a template:',
40
+ choices: [
41
+ { name: 'Minimal (Express + TS only)', value: 'minimal' },
42
+ { name: 'Standard (+ ESLint, Prettier, Vitest, Husky)', value: 'standard' },
43
+ { name: 'Advanced (+ Docker, Swagger, Zod, Pino, CI/CD)', value: 'advanced' },
44
+ ],
45
+ default: 'standard',
46
+ },
47
+ {
48
+ type: 'confirm',
49
+ name: 'docker',
50
+ message: 'Include Docker setup?',
51
+ default: false,
52
+ when: (ans) => ans.template !== 'minimal',
53
+ },
54
+ ]);
55
+ projectName = answers.name;
56
+ options.template = answers.template;
57
+ if (answers.docker !== undefined) options.docker = answers.docker;
58
+ }
59
+
60
+ const targetDir = join(process.cwd(), projectName);
61
+
62
+ if (existsSync(targetDir)) {
63
+ console.error(chalk.red(`\nโŒ Directory "${projectName}" already exists.\n`));
64
+ process.exit(1);
65
+ }
66
+
67
+ // Fancy header
68
+ console.log(chalk.cyan(figlet.textSync('Express TS', { font: 'Small' })));
69
+ console.log(chalk.gray('Scaffolding your project...\n'));
70
+
71
+ // Step 1: Copy template
72
+ const spinner = ora('Copying template files...').start();
73
+ const templateDir = join(__dirname, '..', 'templates', options.template);
74
+
75
+ if (!existsSync(templateDir)) {
76
+ spinner.fail(chalk.red(`Template "${options.template}" not found.`));
77
+ process.exit(1);
78
+ }
79
+
80
+ mkdirSync(targetDir, { recursive: true });
81
+ cpSync(templateDir, targetDir, { recursive: true });
82
+ spinner.succeed('Template files copied');
83
+
84
+ // Step 2: Update package.json name
85
+ const pkgPath = join(targetDir, 'package.json');
86
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
87
+ pkg.name = projectName;
88
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
89
+
90
+ // Step 3: Conditional Docker files
91
+ if (!options.docker) {
92
+ const dockerFiles = ['Dockerfile', 'docker-compose.yml', '.dockerignore'];
93
+ dockerFiles.forEach((f) => {
94
+ const p = join(targetDir, f);
95
+ if (existsSync(p)) rmSync(p);
96
+ });
97
+ }
98
+
99
+ // Step 4: Git init
100
+ if (!options.skipGit) {
101
+ const gitSpinner = ora('Initializing Git repository...').start();
102
+ try {
103
+ execSync('git init', { cwd: targetDir, stdio: 'ignore' });
104
+ execSync('git checkout -b main', { cwd: targetDir, stdio: 'ignore' });
105
+ gitSpinner.succeed('Git repository initialized');
106
+ } catch {
107
+ gitSpinner.warn('Git initialization failed');
108
+ }
109
+ }
110
+
111
+ // Step 5: Install deps
112
+ if (!options.skipInstall) {
113
+ const installSpinner = ora('Installing dependencies...').start();
114
+ try {
115
+ execSync('npm install', { cwd: targetDir, stdio: 'ignore' });
116
+ installSpinner.succeed('Dependencies installed');
117
+ } catch {
118
+ installSpinner.warn('Dependency installation failed. Run `npm install` manually.');
119
+ }
120
+ }
121
+
122
+ // Step 6: Setup Husky (standard & advanced only)
123
+ if (!options.skipInstall && options.template !== 'minimal') {
124
+ const huskySpinner = ora('Setting up Git hooks...').start();
125
+ try {
126
+ execSync('npx husky init', { cwd: targetDir, stdio: 'ignore' });
127
+ const preCommit = join(targetDir, '.husky', 'pre-commit');
128
+ writeFileSync(preCommit, 'npx lint-staged\n', { mode: 0o755 });
129
+
130
+ // Pre-push for advanced
131
+ if (options.template === 'advanced') {
132
+ const prePush = join(targetDir, '.husky', 'pre-push');
133
+ writeFileSync(prePush, 'npm run test:run\n', { mode: 0o755 });
134
+ }
135
+ huskySpinner.succeed('Git hooks configured');
136
+ } catch {
137
+ huskySpinner.warn('Husky setup failed. Run `npx husky init` manually.');
138
+ }
139
+ }
140
+
141
+ // Step 7: Generate README for advanced
142
+ if (options.template === 'advanced') {
143
+ const readme = generateReadme(projectName, options);
144
+ writeFileSync(join(targetDir, 'README.md'), readme);
145
+ }
146
+
147
+ // Success message
148
+ console.log(chalk.green.bold(`\nโœ… Successfully created ${projectName}\n`));
149
+ console.log(chalk.white('Get started:'));
150
+ console.log(chalk.cyan(` cd ${projectName}`));
151
+ if (options.skipInstall) console.log(chalk.cyan(' npm install'));
152
+ console.log(chalk.cyan(' npm run dev'));
153
+ console.log(chalk.gray('\n๐Ÿ“– Read the README.md for more commands.\n'));
154
+ });
155
+
156
+ function generateReadme(name, opts) {
157
+ return `# ${name}
158
+
159
+ A production-ready Express + TypeScript API.
160
+
161
+ ## ๐Ÿš€ Getting Started
162
+
163
+ \`\`\`bash
164
+ npm install
165
+ npm run dev
166
+ \`\`\`
167
+
168
+ ## ๐Ÿ“œ Scripts
169
+
170
+ | Command | Description |
171
+ |---------|-------------|
172
+ | \`npm run dev\` | Start development server with hot reload |
173
+ | \`npm run build\` | Compile TypeScript to \`dist/\` |
174
+ | \`npm run start\` | Run production build |
175
+ | \`npm test\` | Run tests in watch mode |
176
+ | \`npm run test:run\` | Run tests once |
177
+ | \`npm run test:coverage\` | Run tests with coverage report |
178
+ | \`npm run lint\` | Lint source files |
179
+ | \`npm run lint:fix\` | Fix linting issues |
180
+ | \`npm run format\` | Format code with Prettier |
181
+
182
+ ${opts.docker ? `## ๐Ÿณ Docker
183
+
184
+ \`\`\`bash
185
+ docker-compose up --build
186
+ \`\`\`
187
+ ` : ''}
188
+ ## ๐Ÿ“ Project Structure
189
+
190
+ \`\`\`
191
+ src/
192
+ โ”œโ”€โ”€ config/ # Environment, logger, swagger
193
+ โ”œโ”€โ”€ middleware/ # Security, error handling, validation
194
+ โ”œโ”€โ”€ routes/ # API routes
195
+ โ””โ”€โ”€ index.ts # Entry point
196
+ \`\`\`
197
+
198
+ ## ๐Ÿ”’ Environment Variables
199
+
200
+ | Variable | Description | Default |
201
+ |----------|-------------|---------|
202
+ | \`NODE_ENV\` | Runtime environment | \`development\` |
203
+ | \`PORT\` | Server port | \`3000\` |
204
+ | \`LOG_LEVEL\` | Pino log level | \`info\` |
205
+
206
+ ## License
207
+
208
+ MIT
209
+ `;
210
+ }
211
+
212
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "create-exts-app",
3
+ "version": "1.0.0",
4
+ "description": "Scaffold a production-ready Express + TypeScript API with professional tooling",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-exts-app": "bin/cli.js"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "templates/"
12
+ ],
13
+ "engines": {
14
+ "node": ">=20.0.0"
15
+ },
16
+ "keywords": [
17
+ "express",
18
+ "typescript",
19
+ "boilerplate",
20
+ "scaffold",
21
+ "vitest",
22
+ "eslint",
23
+ "prettier",
24
+ "husky",
25
+ "cli"
26
+ ],
27
+ "author": "Your Name",
28
+ "license": "MIT",
29
+ "dependencies": {
30
+ "chalk": "^5.3.0",
31
+ "commander": "^12.0.0",
32
+ "figlet": "^1.7.0",
33
+ "inquirer": "^12.0.0",
34
+ "ora": "^8.0.0"
35
+ }
36
+ }
@@ -0,0 +1,8 @@
1
+ node_modules
2
+ dist
3
+ .git
4
+ .env
5
+ *.log
6
+ coverage
7
+ .vscode
8
+ .github
@@ -0,0 +1,9 @@
1
+ root = true
2
+
3
+ [*]
4
+ charset = utf-8
5
+ end_of_line = lf
6
+ indent_size = 2
7
+ indent_style = space
8
+ insert_final_newline = true
9
+ trim_trailing_whitespace = true
@@ -0,0 +1 @@
1
+ * text=auto eol=lf
@@ -0,0 +1,32 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main, develop]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ node-version: [22.x, 24.x, 26.x]
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: actions/setup-node@v4
18
+ with:
19
+ node-version: ${{ matrix.node-version }}
20
+ cache: 'npm'
21
+ - run: npm ci
22
+ - run: npm run lint
23
+ - run: npm run test:run
24
+ - run: npm run build
25
+
26
+ docker:
27
+ runs-on: ubuntu-latest
28
+ needs: test
29
+ steps:
30
+ - uses: actions/checkout@v4
31
+ - name: Build Docker image
32
+ run: docker build -t myapp .
@@ -0,0 +1,19 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags: ['v*']
6
+
7
+ jobs:
8
+ publish:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+ - uses: actions/setup-node@v4
13
+ with:
14
+ registry-url: 'https://registry.npmjs.org'
15
+ - run: npm ci
16
+ - run: npm run build
17
+ - run: npm publish --access public
18
+ env:
19
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -0,0 +1 @@
1
+ npx lint-staged
@@ -0,0 +1 @@
1
+ npm run test:run
@@ -0,0 +1,6 @@
1
+ {
2
+ "recommendations": [
3
+ "dbaeumer.vscode-eslint",
4
+ "esbenp.prettier-vscode"
5
+ ]
6
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
3
+ "editor.formatOnSave": true,
4
+ "editor.codeActionsOnSave": {
5
+ "source.fixAll.eslint": "explicit"
6
+ },
7
+ "typescript.preferences.importModuleSpecifier": "relative"
8
+ }
@@ -0,0 +1,17 @@
1
+ # Build stage
2
+ FROM node:24-alpine AS builder
3
+ WORKDIR /app
4
+ COPY package*.json ./
5
+ RUN npm ci
6
+ COPY . .
7
+ RUN npm run build
8
+
9
+ # Production stage
10
+ FROM node:24-alpine
11
+ WORKDIR /app
12
+ ENV NODE_ENV=production
13
+ COPY package*.json ./
14
+ RUN npm ci --omit=dev
15
+ COPY --from=builder /app/dist ./dist
16
+ EXPOSE 3000
17
+ CMD ["node", "dist/index.js"]
@@ -0,0 +1,12 @@
1
+ version: '3.8'
2
+
3
+ services:
4
+ app:
5
+ build: .
6
+ ports:
7
+ - "3000:3000"
8
+ environment:
9
+ - NODE_ENV=production
10
+ - PORT=3000
11
+ - LOG_LEVEL=info
12
+ restart: unless-stopped
@@ -0,0 +1,35 @@
1
+ // @ts-check
2
+ import js from '@eslint/js';
3
+ import tseslint from 'typescript-eslint';
4
+ import globals from 'globals';
5
+
6
+ export default tseslint.config(
7
+ js.configs.recommended,
8
+ ...tseslint.configs.recommended,
9
+ ...tseslint.configs.recommendedTypeChecked,
10
+ {
11
+ languageOptions: {
12
+ parserOptions: {
13
+ projectService: true,
14
+ tsconfigDirName: import.meta.dirname,
15
+ },
16
+ globals: {
17
+ ...globals.node,
18
+ ...globals.es2024,
19
+ },
20
+ },
21
+ rules: {
22
+ '@typescript-eslint/no-unused-vars': [
23
+ 'error',
24
+ { argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
25
+ ],
26
+ '@typescript-eslint/explicit-function-return-type': 'off',
27
+ '@typescript-eslint/explicit-module-boundary-types': 'off',
28
+ 'prefer-const': 'error',
29
+ 'no-console': 'warn',
30
+ },
31
+ },
32
+ {
33
+ ignores: ['dist/', 'node_modules/', 'coverage/', '*.config.*'],
34
+ }
35
+ );
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "my-project",
3
+ "version": "1.0.0",
4
+ "description": "Production-ready Express + TypeScript API",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=20.0.0"
8
+ },
9
+ "scripts": {
10
+ "dev": "tsx watch src/index.ts",
11
+ "build": "tsc && tsc-alias",
12
+ "start": "node dist/index.js",
13
+ "test": "vitest",
14
+ "test:run": "vitest run",
15
+ "test:coverage": "vitest run --coverage",
16
+ "lint": "eslint src tests",
17
+ "lint:fix": "eslint src tests --fix",
18
+ "format": "prettier --write .",
19
+ "format:check": "prettier --check .",
20
+ "prepare": "husky"
21
+ },
22
+ "lint-staged": {
23
+ "*.{ts,tsx,js,jsx,mjs,cjs}": [
24
+ "eslint --fix",
25
+ "prettier --write"
26
+ ]
27
+ },
28
+ "dependencies": {
29
+ "cors": "^2.8.5",
30
+ "express": "^5.2.0",
31
+ "express-rate-limit": "^7.0.0",
32
+ "helmet": "^8.0.0",
33
+ "pino": "^9.0.0",
34
+ "pino-pretty": "^13.0.0",
35
+ "swagger-ui-express": "^5.0.0",
36
+ "zod": "^3.23.0"
37
+ },
38
+ "devDependencies": {
39
+ "@types/cors": "^2.8.0",
40
+ "@types/express": "^5.0.0",
41
+ "@types/node": "^22.0.0",
42
+ "@types/supertest": "^6.0.0",
43
+ "@types/swagger-ui-express": "^4.1.0",
44
+ "eslint": "^10.9.1",
45
+ "globals": "^15.0.0",
46
+ "husky": "^9.1.7",
47
+ "lint-staged": "^15.0.0",
48
+ "prettier": "^3.9.6",
49
+ "supertest": "^7.0.0",
50
+ "tsc-alias": "^1.8.0",
51
+ "tsx": "^4.0.0",
52
+ "typescript": "^5.7.0",
53
+ "typescript-eslint": "^8.0.0",
54
+ "vitest": "^4.1.11"
55
+ }
56
+ }
@@ -0,0 +1,10 @@
1
+ /** @type {import("prettier").Config} */
2
+ export default {
3
+ semi: true,
4
+ trailingComma: 'all',
5
+ singleQuote: true,
6
+ printWidth: 100,
7
+ tabWidth: 2,
8
+ useTabs: false,
9
+ endOfLine: 'lf',
10
+ };
@@ -0,0 +1,20 @@
1
+ import express from 'express';
2
+ import { applySecurityMiddleware } from '@middleware/security.js';
3
+ import { errorHandler } from '@middleware/errorHandler.js';
4
+ import { setupSwagger } from '@config/swagger.js';
5
+ import healthRouter from '@routes/health.js';
6
+
7
+ export const app = express();
8
+
9
+ app.use(express.json());
10
+
11
+ applySecurityMiddleware(app);
12
+ setupSwagger(app);
13
+
14
+ app.use('/health', healthRouter);
15
+
16
+ app.use((_req, res) => {
17
+ res.status(404).json({ success: false, error: { message: 'Not found' } });
18
+ });
19
+
20
+ app.use(errorHandler);
@@ -0,0 +1,10 @@
1
+ import { z } from 'zod';
2
+
3
+ const envSchema = z.object({
4
+ NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
5
+ PORT: z.string().transform(Number).default('3000'),
6
+ LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
7
+ DATABASE_URL: z.string().optional(),
8
+ });
9
+
10
+ export const env = envSchema.parse(process.env);
@@ -0,0 +1,11 @@
1
+ import pino from 'pino';
2
+ import { env } from './env.js';
3
+
4
+ export const logger = pino({
5
+ level: env.LOG_LEVEL,
6
+ transport:
7
+ env.NODE_ENV === 'development'
8
+ ? { target: 'pino-pretty', options: { colorize: true } }
9
+ : undefined,
10
+ base: { pid: process.pid },
11
+ });
@@ -0,0 +1,39 @@
1
+ import swaggerUi from 'swagger-ui-express';
2
+ import type { Express } from 'express';
3
+
4
+ const swaggerDocument = {
5
+ openapi: '3.0.0',
6
+ info: {
7
+ title: 'Express TS API',
8
+ version: '1.0.0',
9
+ description: 'Auto-generated API documentation',
10
+ },
11
+ servers: [{ url: 'http://localhost:3000' }],
12
+ paths: {
13
+ '/health': {
14
+ get: {
15
+ summary: 'Health check',
16
+ responses: {
17
+ '200': {
18
+ description: 'Service is healthy',
19
+ content: {
20
+ 'application/json': {
21
+ schema: {
22
+ type: 'object',
23
+ properties: {
24
+ status: { type: 'string', example: 'ok' },
25
+ timestamp: { type: 'string', format: 'date-time' },
26
+ },
27
+ },
28
+ },
29
+ },
30
+ },
31
+ },
32
+ },
33
+ },
34
+ },
35
+ };
36
+
37
+ export const setupSwagger = (app: Express) => {
38
+ app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument));
39
+ };
@@ -0,0 +1,8 @@
1
+ import { app } from './app.js';
2
+ import { env } from '@config/env.js';
3
+ import { logger } from '@config/logger.js';
4
+
5
+ app.listen(env.PORT, () => {
6
+ logger.info(`๐Ÿš€ Server running on http://localhost:${env.PORT}`);
7
+ logger.info(`๐Ÿ“– API docs available at http://localhost:${env.PORT}/api-docs`);
8
+ });
@@ -0,0 +1,18 @@
1
+ import type { ErrorRequestHandler } from 'express';
2
+ import { logger } from '@config/logger.js';
3
+ import { env } from '@config/env.js';
4
+
5
+ export const errorHandler: ErrorRequestHandler = (err, _req, res, _next) => {
6
+ logger.error(err);
7
+
8
+ const statusCode = err.statusCode || err.status || 500;
9
+ const message = err.message || 'Internal Server Error';
10
+
11
+ res.status(statusCode).json({
12
+ success: false,
13
+ error: {
14
+ message,
15
+ ...(env.NODE_ENV === 'development' && { stack: err.stack }),
16
+ },
17
+ });
18
+ };
@@ -0,0 +1,17 @@
1
+ import helmet from 'helmet';
2
+ import cors from 'cors';
3
+ import rateLimit from 'express-rate-limit';
4
+ import type { Express } from 'express';
5
+
6
+ export const applySecurityMiddleware = (app: Express) => {
7
+ app.use(helmet());
8
+ app.use(cors({ origin: process.env.CORS_ORIGIN || '*' }));
9
+ app.use(
10
+ rateLimit({
11
+ windowMs: 15 * 60 * 1000,
12
+ max: 100,
13
+ standardHeaders: true,
14
+ legacyHeaders: false,
15
+ }),
16
+ );
17
+ };
@@ -0,0 +1,26 @@
1
+ import type { Request, Response, NextFunction } from 'express';
2
+ import { z, type ZodSchema } from 'zod';
3
+
4
+ export const validate = (schema: ZodSchema) => {
5
+ return (req: Request, res: Response, next: NextFunction) => {
6
+ try {
7
+ schema.parse({
8
+ body: req.body,
9
+ query: req.query,
10
+ params: req.params,
11
+ });
12
+ next();
13
+ } catch (err) {
14
+ if (err instanceof z.ZodError) {
15
+ return res.status(400).json({
16
+ success: false,
17
+ error: {
18
+ message: 'Validation failed',
19
+ details: err.errors,
20
+ },
21
+ });
22
+ }
23
+ next(err);
24
+ }
25
+ };
26
+ };
@@ -0,0 +1,9 @@
1
+ import { Router } from 'express';
2
+
3
+ const router = Router();
4
+
5
+ router.get('/', (_req, res) => {
6
+ res.json({ status: 'ok', timestamp: new Date().toISOString() });
7
+ });
8
+
9
+ export default router;
@@ -0,0 +1,26 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import request from 'supertest';
3
+ import { app } from '../src/app.js';
4
+
5
+ describe('App', () => {
6
+ it('GET /health returns status ok', async () => {
7
+ const res = await request(app).get('/health');
8
+
9
+ expect(res.status).toBe(200);
10
+ expect(res.body).toHaveProperty('status', 'ok');
11
+ expect(res.body).toHaveProperty('timestamp');
12
+ });
13
+
14
+ it('returns 404 for unknown routes', async () => {
15
+ const res = await request(app).get('/unknown');
16
+
17
+ expect(res.status).toBe(404);
18
+ expect(res.body.success).toBe(false);
19
+ expect(res.body.error).toHaveProperty('message', 'Not found');
20
+ });
21
+
22
+ it('serves swagger docs', async () => {
23
+ const res = await request(app).get('/api-docs');
24
+ expect(res.status).toBe(301);
25
+ });
26
+ });
@@ -0,0 +1,9 @@
1
+ import { beforeAll, afterAll } from 'vitest';
2
+
3
+ beforeAll(() => {
4
+ process.env.NODE_ENV = 'test';
5
+ });
6
+
7
+ afterAll(() => {
8
+ // cleanup if needed
9
+ });
@@ -0,0 +1,47 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2024",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": [
7
+ "ES2024"
8
+ ],
9
+ "outDir": "./dist",
10
+ "rootDir": "./src",
11
+ "baseUrl": ".",
12
+ "paths": {
13
+ "@/*": [
14
+ "src/*"
15
+ ],
16
+ "@config/*": [
17
+ "src/config/*"
18
+ ],
19
+ "@middleware/*": [
20
+ "src/middleware/*"
21
+ ],
22
+ "@routes/*": [
23
+ "src/routes/*"
24
+ ]
25
+ },
26
+ "strict": true,
27
+ "esModuleInterop": true,
28
+ "skipLibCheck": true,
29
+ "forceConsistentCasingInFileNames": true,
30
+ "resolveJsonModule": true,
31
+ "declaration": true,
32
+ "declarationMap": true,
33
+ "sourceMap": true,
34
+ "noUnusedLocals": true,
35
+ "noUnusedParameters": true,
36
+ "noImplicitReturns": true,
37
+ "noFallthroughCasesInSwitch": true
38
+ },
39
+ "include": [
40
+ "src/**/*"
41
+ ],
42
+ "exclude": [
43
+ "node_modules",
44
+ "dist",
45
+ "tests"
46
+ ]
47
+ }
@@ -0,0 +1,25 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: 'node',
7
+ include: ['tests/**/*.test.ts'],
8
+ coverage: {
9
+ provider: 'v8',
10
+ reporter: ['text', 'json', 'html'],
11
+ thresholds: {
12
+ lines: 80,
13
+ functions: 80,
14
+ branches: 70,
15
+ statements: 80,
16
+ },
17
+ exclude: [
18
+ 'tests/',
19
+ 'dist/',
20
+ '**/*.config.*',
21
+ 'src/config/swagger.ts',
22
+ ],
23
+ },
24
+ },
25
+ });
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "my-project",
3
+ "version": "1.0.0",
4
+ "description": "Minimal Express + TypeScript API",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "tsx watch src/index.ts",
8
+ "build": "tsc",
9
+ "start": "node dist/index.js"
10
+ },
11
+ "engines": {
12
+ "node": ">=20.0.0"
13
+ },
14
+ "dependencies": {
15
+ "express": "^5.2.0"
16
+ },
17
+ "devDependencies": {
18
+ "@types/express": "^5.0.0",
19
+ "@types/node": "^22.0.0",
20
+ "tsx": "^4.0.0",
21
+ "typescript": "^7.0.0"
22
+ }
23
+ }
@@ -0,0 +1,13 @@
1
+ import express from 'express';
2
+
3
+ export const app = express();
4
+
5
+ app.use(express.json());
6
+
7
+ app.get('/health', (_req, res) => {
8
+ res.json({ status: 'ok', timestamp: new Date().toISOString() });
9
+ });
10
+
11
+ app.use((_req, res) => {
12
+ res.status(404).json({ error: 'Not found' });
13
+ });
@@ -0,0 +1,7 @@
1
+ import { app } from './app.js';
2
+
3
+ const PORT = Number(process.env.PORT) || 3000;
4
+
5
+ app.listen(PORT, () => {
6
+ console.log(`Server running on http://localhost:${PORT}`);
7
+ });
@@ -0,0 +1,26 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2024",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": [
7
+ "ES2024"
8
+ ],
9
+ "outDir": "./dist",
10
+ "rootDir": "./src",
11
+ "strict": true,
12
+ "esModuleInterop": true,
13
+ "skipLibCheck": true,
14
+ "forceConsistentCasingInFileNames": true,
15
+ "resolveJsonModule": true,
16
+ "declaration": true,
17
+ "sourceMap": true
18
+ },
19
+ "include": [
20
+ "src/**/*"
21
+ ],
22
+ "exclude": [
23
+ "node_modules",
24
+ "dist"
25
+ ]
26
+ }
@@ -0,0 +1,9 @@
1
+ root = true
2
+
3
+ [*]
4
+ charset = utf-8
5
+ end_of_line = lf
6
+ indent_size = 2
7
+ indent_style = space
8
+ insert_final_newline = true
9
+ trim_trailing_whitespace = true
@@ -0,0 +1 @@
1
+ npx lint-staged
@@ -0,0 +1,6 @@
1
+ {
2
+ "recommendations": [
3
+ "dbaeumer.vscode-eslint",
4
+ "esbenp.prettier-vscode"
5
+ ]
6
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
3
+ "editor.formatOnSave": true,
4
+ "editor.codeActionsOnSave": {
5
+ "source.fixAll.eslint": "explicit"
6
+ },
7
+ "typescript.preferences.importModuleSpecifier": "relative"
8
+ }
@@ -0,0 +1,35 @@
1
+ // @ts-check
2
+ import js from '@eslint/js';
3
+ import tseslint from 'typescript-eslint';
4
+ import globals from 'globals';
5
+
6
+ export default tseslint.config(
7
+ js.configs.recommended,
8
+ ...tseslint.configs.recommended,
9
+ ...tseslint.configs.recommendedTypeChecked,
10
+ {
11
+ languageOptions: {
12
+ parserOptions: {
13
+ projectService: true,
14
+ tsconfigDirName: import.meta.dirname,
15
+ },
16
+ globals: {
17
+ ...globals.node,
18
+ ...globals.es2024,
19
+ },
20
+ },
21
+ rules: {
22
+ '@typescript-eslint/no-unused-vars': [
23
+ 'error',
24
+ { argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
25
+ ],
26
+ '@typescript-eslint/explicit-function-return-type': 'off',
27
+ '@typescript-eslint/explicit-module-boundary-types': 'off',
28
+ 'prefer-const': 'error',
29
+ 'no-console': 'warn',
30
+ },
31
+ },
32
+ {
33
+ ignores: ['dist/', 'node_modules/', 'coverage/', '*.config.*'],
34
+ }
35
+ );
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "my-project",
3
+ "version": "1.0.0",
4
+ "description": "Express + TypeScript API with ESLint, Prettier, Husky, Vitest",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=20.0.0"
8
+ },
9
+ "scripts": {
10
+ "dev": "tsx watch src/index.ts",
11
+ "build": "tsc",
12
+ "start": "node dist/index.js",
13
+ "test": "vitest",
14
+ "test:run": "vitest run",
15
+ "test:coverage": "vitest run --coverage",
16
+ "lint": "eslint src tests",
17
+ "lint:fix": "eslint src tests --fix",
18
+ "format": "prettier --write .",
19
+ "format:check": "prettier --check .",
20
+ "prepare": "husky"
21
+ },
22
+ "lint-staged": {
23
+ "*.{ts,tsx,js,jsx,mjs,cjs}": [
24
+ "eslint --fix",
25
+ "prettier --write"
26
+ ]
27
+ },
28
+ "dependencies": {
29
+ "express": "^5.2.0"
30
+ },
31
+ "devDependencies": {
32
+ "@types/express": "^5.0.0",
33
+ "@types/node": "^22.0.0",
34
+ "@types/supertest": "^6.0.0",
35
+ "eslint": "^10.9.1",
36
+ "globals": "^15.0.0",
37
+ "husky": "^9.1.7",
38
+ "lint-staged": "^15.0.0",
39
+ "prettier": "^3.9.6",
40
+ "supertest": "^7.0.0",
41
+ "tsx": "^4.0.0",
42
+ "typescript": "^5.7.0",
43
+ "typescript-eslint": "^8.0.0",
44
+ "vitest": "^4.1.11"
45
+ }
46
+ }
@@ -0,0 +1,10 @@
1
+ /** @type {import("prettier").Config} */
2
+ export default {
3
+ semi: true,
4
+ trailingComma: 'all',
5
+ singleQuote: true,
6
+ printWidth: 100,
7
+ tabWidth: 2,
8
+ useTabs: false,
9
+ endOfLine: 'lf',
10
+ };
@@ -0,0 +1,13 @@
1
+ import express from 'express';
2
+
3
+ export const app = express();
4
+
5
+ app.use(express.json());
6
+
7
+ app.get('/health', (_req, res) => {
8
+ res.json({ status: 'ok', timestamp: new Date().toISOString() });
9
+ });
10
+
11
+ app.use((_req, res) => {
12
+ res.status(404).json({ error: 'Not found' });
13
+ });
@@ -0,0 +1,7 @@
1
+ import { app } from './app.js';
2
+
3
+ const PORT = Number(process.env.PORT) || 3000;
4
+
5
+ app.listen(PORT, () => {
6
+ console.log(`๐Ÿš€ Server running on http://localhost:${PORT}`);
7
+ });
@@ -0,0 +1,20 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import request from 'supertest';
3
+ import { app } from '../src/app.js';
4
+
5
+ describe('App', () => {
6
+ it('GET /health returns status ok', async () => {
7
+ const res = await request(app).get('/health');
8
+
9
+ expect(res.status).toBe(200);
10
+ expect(res.body).toHaveProperty('status', 'ok');
11
+ expect(res.body).toHaveProperty('timestamp');
12
+ });
13
+
14
+ it('returns 404 for unknown routes', async () => {
15
+ const res = await request(app).get('/unknown');
16
+
17
+ expect(res.status).toBe(404);
18
+ expect(res.body).toEqual({ error: 'Not found' });
19
+ });
20
+ });
@@ -0,0 +1,32 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2024",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": [
7
+ "ES2024"
8
+ ],
9
+ "outDir": "./dist",
10
+ "rootDir": "./src",
11
+ "strict": true,
12
+ "esModuleInterop": true,
13
+ "skipLibCheck": true,
14
+ "forceConsistentCasingInFileNames": true,
15
+ "resolveJsonModule": true,
16
+ "declaration": true,
17
+ "declarationMap": true,
18
+ "sourceMap": true,
19
+ "noUnusedLocals": true,
20
+ "noUnusedParameters": true,
21
+ "noImplicitReturns": true,
22
+ "noFallthroughCasesInSwitch": true
23
+ },
24
+ "include": [
25
+ "src/**/*"
26
+ ],
27
+ "exclude": [
28
+ "node_modules",
29
+ "dist",
30
+ "tests"
31
+ ]
32
+ }
@@ -0,0 +1,14 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: 'node',
7
+ include: ['tests/**/*.test.ts'],
8
+ coverage: {
9
+ provider: 'v8',
10
+ reporter: ['text', 'json', 'html'],
11
+ exclude: ['tests/', 'dist/', '*.config.*'],
12
+ },
13
+ },
14
+ });