Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased

- Add an Express-only `--openapi` option that generates `docs/openapi.yaml` and documents it in the generated README.
- Add `--typescript` support for generated Express apps with `src/index.ts`, `tsconfig.json`, `tsx` development, `tsc` builds to `dist`, generated TypeScript tests, and Docker support.
- Keep JavaScript output unchanged by default and reject `--typescript` with the plain Node framework.

Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ generated app unless `--skip-install` is used.
| `--framework node` | Plain Node.js HTTP server with `/`, `/about`, `/contact`, and `/health`, plus MVC folders, Docker files, `.gitignore`, generated `README.md`, and `package.json`. |
| `--view ejs`, `pug`, or `hbs` | Adds the selected Express view template and renders it from `/`. |
| `--db` | Adds Mongoose, `config/mongoose.js`, and `MONGODB_URI` in `.env.example` for Express apps. |
| `--openapi` | Adds `docs/openapi.yaml`, a static OpenAPI 3.0 spec for the generated Express routes. |

Generated apps include `npm start` and `npm run dev`. Express apps also include
`npm test`. TypeScript Express apps also include `npm run build`.
Expand All @@ -65,6 +66,7 @@ servergen [options] [name]
-f, --framework <type> framework: express | node (default: "express")
-v, --view <type> view engine (express only): ejs | pug | hbs
--db add Mongoose and a MongoDB config (express only)
--openapi generate an OpenAPI spec file (express only)
--typescript generate an Express TypeScript app
-p, --port <number> port for the generated app (1-65535) (default: "3000")
--skip-install skip the npm install step
Expand All @@ -79,6 +81,7 @@ npx servergen@latest my-api
npx servergen@latest my-api --framework node
npx servergen@latest my-api --view ejs
npx servergen@latest my-api --db
npx servergen@latest my-api --openapi
npx servergen@latest my-api --typescript
npx servergen@latest my-api --port 8080
npx servergen@latest my-api --skip-install
Expand Down
3 changes: 3 additions & 0 deletions bin/servergen.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ program
.option('-f, --framework <type>', 'framework: express | node', 'express')
.option('-v, --view <type>', 'view engine (express only): ejs | pug | hbs')
.option('--db', 'add Mongoose and a MongoDB config (express only)')
.option('--openapi', 'generate an OpenAPI spec file (express only)')
.option('--typescript', 'generate an Express TypeScript app')
.option('-p, --port <number>', 'port for the generated app (1-65535)', '3000')
.option('--skip-install', 'skip the npm install step')
Expand All @@ -43,6 +44,7 @@ Examples:
$ servergen my-api -f node create a Node app
$ servergen my-api -v ejs Express app with the EJS view engine
$ servergen my-api --db Express app with Mongoose/MongoDB
$ servergen my-api --openapi Express app with docs/openapi.yaml
$ servergen my-api --typescript Express app with TypeScript
$ servergen my-api -p 8080 use a custom port
$ servergen my-api --skip-install scaffold without running npm install
Expand Down Expand Up @@ -105,6 +107,7 @@ const main = async () => {
framework: options.framework,
view: options.view,
db: options.db,
openapi: options.openapi,
port,
skipInstall,
typescript: options.typescript,
Expand Down
4 changes: 4 additions & 0 deletions docs/examples/express.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ Generated files and directories include:
- `package.json` with `start`, `dev`, and `test` scripts
- `.env.example`, `.gitignore`, `.dockerignore`, `Dockerfile`, and `README.md`

Pass `--openapi` to also generate `docs/openapi.yaml`, a static OpenAPI 3.0
spec for `/`, `POST /`, and `/health`. The spec uses the selected app name and
custom `--port` value in its metadata and local server URL.

Because the command runs installation by default, npm also creates `node_modules/` and `package-lock.json` inside `hello-express/`.

The generated Express app depends on `express`, `cors`, and `dotenv`. It does not generate auth, ORM models, or production deployment configuration.
Expand Down
11 changes: 11 additions & 0 deletions lib/app_generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class AppGenerator {
* @param {string} options.framework - The framework type ('node' or 'express').
* @param {string|null} options.view - The view engine name.
* @param {boolean} options.db - Whether to include database configuration.
* @param {boolean} options.openapi - Whether to generate an OpenAPI spec.
* @param {number} options.port - The port number for the app.
* @param {boolean} options.skipInstall - Whether to skip npm install.
* @param {boolean} options.typescript - Whether to generate a TypeScript app.
Expand All @@ -33,6 +34,7 @@ class AppGenerator {
this.framework = options.framework || 'express';
this.view = options.view;
this.db = options.db;
this.openapi = options.openapi || false;
this.port = options.port || 3000;
this.skipInstall = options.skipInstall || false;
this.typescript = options.typescript || false;
Expand All @@ -57,6 +59,7 @@ class AppGenerator {
this.logger?.debug('Starting app generation', {
appName: this.appName,
framework: this.framework,
openapi: this.openapi,
port: this.port,
skipInstall: this.skipInstall,
typescript: this.typescript,
Expand Down Expand Up @@ -167,6 +170,7 @@ class AppGenerator {
addSupportFiles() {
const supportOptions = {
db: Boolean(this.db),
openapi: Boolean(this.openapi),
port: this.port,
typescript: this.typescript,
};
Expand All @@ -178,6 +182,13 @@ class AppGenerator {
appName: this.appName,
});
this.fileCreator.addEnvExample(this.folderDir, this.templatesDir, supportOptions);
if (this.framework === 'express' && this.openapi) {
this.fileCreator.addOpenApiSpec(this.folderDir, {
appName: this.appName,
port: this.port,
view: this.view,
});
}
}

/**
Expand Down
179 changes: 179 additions & 0 deletions lib/file_generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -379,10 +379,188 @@ const addReadme = (folderDir, templatesDir, options = {}) => {
.replace(/http:\/\/localhost:3000/g, `http://localhost:${options.port}`)
.replace(/http:\/\/127\.0\.0\.1:3000/g, `http://127.0.0.1:${options.port}`);
}
if (options.openapi) {
readme += `
## OpenAPI Specification

This app includes a static OpenAPI 3.0 spec at \`docs/openapi.yaml\`.

Inspect it with an OpenAPI viewer, import it into API tooling, or serve it with
your preferred static file middleware if you want browser-accessible docs.
`;
}
fs.writeFileSync(generatedReadmePath, readme, 'utf-8');
}
};

const yamlSingleQuoted = (value) => `'${String(value).replace(/'/g, "''")}'`;

/**
* Adds an OpenAPI specification for generated Express apps.
* @param {string} folderDir - The application directory path.
* @param {Object} options - OpenAPI generation options.
* @param {string} options.appName - Name of the generated app.
* @param {number} options.port - Port configured for the generated app.
* @param {string|null} options.view - The selected Express view engine.
*/
const addOpenApiSpec = (folderDir, options = {}) => {
const appName = options.appName || 'servergen-app';
const port = options.port || 3000;
const hasView = Boolean(options.view);
const rootGetResponse = hasView
? ` description: Rendered welcome page
content:
text/html:
schema:
type: string
example: '<!doctype html><html><body>Welcome to ServerGen!</body></html>'`
: ` description: Welcome response
content:
application/json:
schema:
$ref: '#/components/schemas/WelcomeResponse'
examples:
welcome:
value:
message: Welcome to ServerGen!`;

const spec = `openapi: 3.0.3
info:
title: ${yamlSingleQuoted(`${appName} API`)}
version: 1.0.0
description: ${yamlSingleQuoted(`API specification for the ${appName} Express app generated by ServerGen.`)}
servers:
- url: http://localhost:${port}
description: Local development server
paths:
/:
get:
summary: Root endpoint
operationId: getRoot
responses:
'200':
${rootGetResponse}
'404':
$ref: '#/components/responses/NotFound'
'500':
$ref: '#/components/responses/InternalServerError'
post:
summary: Echo a JSON payload with the welcome response
operationId: postRoot
requestBody:
required: false
content:
application/json:
schema:
type: object
additionalProperties: true
example:
name: ServerGen
responses:
'200':
description: Welcome response with the submitted request body
content:
application/json:
schema:
$ref: '#/components/schemas/PostRootResponse'
examples:
echoedPayload:
value:
message: Welcome to ServerGen!
data:
name: ServerGen
'400':
description: Invalid JSON request body
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'404':
$ref: '#/components/responses/NotFound'
'500':
$ref: '#/components/responses/InternalServerError'
/health:
get:
summary: Health check
operationId: getHealth
responses:
'200':
description: Service health status
content:
application/json:
schema:
$ref: '#/components/schemas/HealthResponse'
examples:
ok:
value:
status: ok
'404':
$ref: '#/components/responses/NotFound'
'500':
$ref: '#/components/responses/InternalServerError'
components:
responses:
NotFound:
description: Route not found
content:
text/html:
schema:
type: string
examples:
notFound:
value: Cannot GET /missing
InternalServerError:
description: Internal server error
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
examples:
internalServerError:
value:
error: Internal Server Error
schemas:
WelcomeResponse:
type: object
required:
- message
properties:
message:
type: string
example: Welcome to ServerGen!
PostRootResponse:
type: object
required:
- message
properties:
message:
type: string
example: Welcome to ServerGen!
data:
type: object
additionalProperties: true
HealthResponse:
type: object
required:
- status
properties:
status:
type: string
example: ok
ErrorResponse:
type: object
required:
- error
properties:
error:
type: string
`;

const docsDir = path.join(folderDir, 'docs');
fs.ensureDirSync(docsDir);
fs.writeFileSync(path.join(docsDir, 'openapi.yaml'), spec, 'utf-8');
};

/**
* Adds .env.example file to the application.
* @param {string} folderDir - The application directory path.
Expand Down Expand Up @@ -416,5 +594,6 @@ export {
addGitIgnore,
addDockerSupport,
addReadme,
addOpenApiSpec,
addEnvExample,
};
4 changes: 4 additions & 0 deletions lib/validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ export const validateOptions = (options, validationRules) => {
errors.push('The --db option (Mongoose) is only supported with the express framework. Use --framework express or remove --db.');
}

if (options.framework === 'node' && options.openapi) {
errors.push('The --openapi option is only supported with the express framework. Use --framework express or remove --openapi.');
}

if (options.framework === 'node' && options.typescript) {
errors.push('The --typescript option is only supported with the express framework. Use --framework express or remove --typescript.');
}
Expand Down
38 changes: 38 additions & 0 deletions tests/integration/integration.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ describe('CLI Integration', () => {
expect(output).toContain('Usage:');
expect(output).toContain('-n, --name');
expect(output).toContain('-f, --framework');
expect(output).toContain('--openapi');
expect(output).toContain('--typescript');
});

Expand All @@ -57,6 +58,7 @@ describe('CLI Integration', () => {
expect(output).toContain('servergen my-api');
expect(output).toContain('-f node');
expect(output).toContain('--db');
expect(output).toContain('--openapi');
expect(output).toContain('--typescript');
});

Expand Down Expand Up @@ -139,6 +141,34 @@ describe('CLI Integration', () => {
expect(fs.existsSync(path.join(testOutput, 'dbtest', 'config'))).toBe(true);
});

it('generates an OpenAPI spec and README docs when --openapi is used', () => {
runCLI('-n openapitest -f express --openapi -p 8080 --skip-install');

const appDir = path.join(testOutput, 'openapitest');
const specPath = path.join(appDir, 'docs', 'openapi.yaml');
const readme = fs.readFileSync(path.join(appDir, 'README.md'), 'utf-8');
const spec = fs.readFileSync(specPath, 'utf-8');

expect(fs.existsSync(specPath)).toBe(true);
expect(spec).toContain('openapi: 3.0.3');
expect(spec).toContain("title: 'openapitest API'");
expect(spec).toContain('url: http://localhost:8080');
expect(spec).toContain('operationId: getRoot');
expect(spec).toContain('operationId: postRoot');
expect(spec).toContain('operationId: getHealth');
expect(spec).toContain('NotFound:');
expect(spec).toContain('InternalServerError:');
expect(readme).toContain('docs/openapi.yaml');
});

it('does not generate an OpenAPI spec by default', () => {
runCLI('-n noopenapi -f express --skip-install');

expect(
fs.existsSync(path.join(testOutput, 'noopenapi', 'docs', 'openapi.yaml'))
).toBe(false);
});

it('generates TypeScript Express app with correct structure and scripts', () => {
runCLI('-n tstest -f express --typescript --skip-install');

Expand Down Expand Up @@ -332,6 +362,14 @@ describe('CLI Integration', () => {
expect(fs.existsSync(path.join(testOutput, 'nodeview'))).toBe(false);
});

it('rejects --openapi with the node framework', () => {
expectCLIError(
'-n nodeopenapi -f node --openapi --skip-install',
'only supported with the express framework'
);
expect(fs.existsSync(path.join(testOutput, 'nodeopenapi'))).toBe(false);
});

it('rejects --typescript with the node framework', () => {
expect(() => runCLI('-n nodets -f node --typescript --skip-install')).toThrow();
expect(fs.existsSync(path.join(testOutput, 'nodets'))).toBe(false);
Expand Down
Loading
Loading