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.
- package/LICENSE +21 -0
- package/README.md +117 -0
- package/bin/cli.js +212 -0
- package/package.json +36 -0
- package/templates/advanced/.dockerignore +8 -0
- package/templates/advanced/.editorconfig +9 -0
- package/templates/advanced/.gitattributes +1 -0
- package/templates/advanced/.github/workflows/ci.yml +32 -0
- package/templates/advanced/.github/workflows/release.yml +19 -0
- package/templates/advanced/.husky/pre-commit +1 -0
- package/templates/advanced/.husky/pre-push +1 -0
- package/templates/advanced/.vscode/extensions.json +6 -0
- package/templates/advanced/.vscode/settings.json +8 -0
- package/templates/advanced/Dockerfile +17 -0
- package/templates/advanced/docker-compose.yml +12 -0
- package/templates/advanced/eslint.config.mjs +35 -0
- package/templates/advanced/package.json +56 -0
- package/templates/advanced/prettier.config.mjs +10 -0
- package/templates/advanced/src/app.ts +20 -0
- package/templates/advanced/src/config/env.ts +10 -0
- package/templates/advanced/src/config/logger.ts +11 -0
- package/templates/advanced/src/config/swagger.ts +39 -0
- package/templates/advanced/src/index.ts +8 -0
- package/templates/advanced/src/middleware/errorHandler.ts +18 -0
- package/templates/advanced/src/middleware/security.ts +17 -0
- package/templates/advanced/src/middleware/validate.ts +26 -0
- package/templates/advanced/src/routes/health.ts +9 -0
- package/templates/advanced/tests/app.test.ts +26 -0
- package/templates/advanced/tests/setup.ts +9 -0
- package/templates/advanced/tsconfig.json +47 -0
- package/templates/advanced/vitest.config.ts +25 -0
- package/templates/minimal/package.json +23 -0
- package/templates/minimal/src/app.ts +13 -0
- package/templates/minimal/src/index.ts +7 -0
- package/templates/minimal/tsconfig.json +26 -0
- package/templates/standard/.editorconfig +9 -0
- package/templates/standard/.husky/pre-commit +1 -0
- package/templates/standard/.vscode/extensions.json +6 -0
- package/templates/standard/.vscode/settings.json +8 -0
- package/templates/standard/eslint.config.mjs +35 -0
- package/templates/standard/package.json +46 -0
- package/templates/standard/prettier.config.mjs +10 -0
- package/templates/standard/src/app.ts +13 -0
- package/templates/standard/src/index.ts +7 -0
- package/templates/standard/tests/app.test.ts +20 -0
- package/templates/standard/tsconfig.json +32 -0
- 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
|
+
[](https://www.npmjs.com/package/create-exts-app)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
[](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 @@
|
|
|
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,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,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,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,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,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,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 @@
|
|
|
1
|
+
npx lint-staged
|
|
@@ -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,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,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
|
+
});
|