diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index c0ceebe..591d1a2 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -2,22 +2,19 @@ name: Deploy Docs
on:
push:
- branches: [main]
+ branches:
+ - main
+ - next
+ - "v*"
paths:
- 'docs/**'
- '.github/workflows/docs.yml'
permissions:
- contents: read
- pages: write
- id-token: write
-
-concurrency:
- group: "pages"
- cancel-in-progress: false
+ contents: write
jobs:
- build:
+ build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
@@ -38,21 +35,32 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile
+ - name: Determine Base Path
+ id: vars
+ run: |
+ BRANCH=${GITHUB_REF#refs/heads/}
+ if [ "$BRANCH" = "main" ]; then
+ echo "BASE=/express-cli/" >> $GITHUB_OUTPUT
+ echo "FOLDER=." >> $GITHUB_OUTPUT
+ elif [ "$BRANCH" = "next" ]; then
+ echo "BASE=/express-cli/next/" >> $GITHUB_OUTPUT
+ echo "FOLDER=next" >> $GITHUB_OUTPUT
+ elif [[ $BRANCH == v* ]]; then
+ # Extract major version, e.g., v1 from v1.x
+ VERSION=$(echo $BRANCH | cut -d'.' -f1)
+ echo "BASE=/express-cli/$VERSION/" >> $GITHUB_OUTPUT
+ echo "FOLDER=$VERSION" >> $GITHUB_OUTPUT
+ fi
+
- name: Build with VitePress
run: pnpm run docs:build
+ env:
+ VITEPRESS_BASE: ${{ steps.vars.outputs.BASE }}
- - name: Upload artifact
- uses: actions/upload-pages-artifact@v3
- with:
- path: docs/.vitepress/dist
-
- deploy:
- environment:
- name: github-pages
- url: ${{ steps.deployment.outputs.page_url }}
- needs: build
- runs-on: ubuntu-latest
- steps:
- name: Deploy to GitHub Pages
- id: deployment
- uses: actions/deploy-pages@v4
+ uses: peaceiris/actions-gh-pages@v4
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ publish_dir: docs/.vitepress/dist
+ destination_dir: ${{ steps.vars.outputs.FOLDER }}
+ keep_files: true # CRITICAL: This keeps other versions alive!
diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml
index 9ee60fe..5e0f916 100644
--- a/.github/workflows/pipeline.yml
+++ b/.github/workflows/pipeline.yml
@@ -32,6 +32,8 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: 20
+ registry-url: 'https://registry.npmjs.org'
+ scope: '@repo'
- name: Setup pnpm
uses: pnpm/action-setup@v3
@@ -55,9 +57,20 @@ jobs:
- name: Version and Publish
run: |
- BRANCH=${GITHUB_REF#refs/heads/}
+ # Detect branch name correctly for both Push and PR
+ if [ "${{ github.event_name }}" = "pull_request" ]; then
+ BRANCH=${{ github.head_ref }}
+ else
+ BRANCH=${GITHUB_REF#refs/heads/}
+ fi
echo "๐ฟ Current branch: $BRANCH"
+ # ๐ SKIP VERSIONING/PUBLISHING ON PULL REQUESTS OR DEV BRANCH
+ if [ "${{ github.event_name }}" = "pull_request" ] || [ "$BRANCH" = "dev" ]; then
+ echo "๐ Pull Request or DEV branch detected. Skipping versioning and publishing logic."
+ exit 0
+ fi
+
# Setup git
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
@@ -99,11 +112,12 @@ jobs:
# Add extra tags if defined
if [ -n "$EXTRA_TAG" ]; then
echo "๐ท๏ธ Adding extra tag: $EXTRA_TAG"
- # Loop through all packages and add the extra tag to the newly published version
- pnpm -r exec sh -c 'npm dist-tag add $(node -p "require(\"./package.json\").name")@$(node -p "require(\"./package.json\").version") '$EXTRA_TAG || echo "โ ๏ธ Could not add extra tag"
+ # Loop through all packages and add the extra tag to the newly published version (skipping private packages)
+ pnpm -r exec sh -c 'if [ "$(node -p "require(\"./package.json\").private")" != "true" ]; then npm dist-tag add $(node -p "require(\"./package.json\").name")@$(node -p "require(\"./package.json\").version") '$EXTRA_TAG'; fi' || echo "โ ๏ธ Could not add extra tag"
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
diff --git a/.husky/pre-commit b/.husky/pre-commit
new file mode 100644
index 0000000..bdce30f
--- /dev/null
+++ b/.husky/pre-commit
@@ -0,0 +1,14 @@
+pnpm run check-types
+pnpm run lint
+pnpm run build
+pnpm run test
+
+# ๐ Mandatory Changeset Check
+# If files in packages/ are changed, a changeset MUST be included.
+if git diff --cached --name-only | grep -q "^packages/"; then
+ if ! git diff --cached --name-only | grep -q "^\.changeset/.*\.md$"; then
+ echo "โ ERROR: You have modified packages but no changeset was found."
+ echo "๐ก Please run 'pnpm changeset' to add one before committing."
+ exit 1
+ fi
+fi
\ No newline at end of file
diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts
index 113feed..2493f4b 100644
--- a/docs/.vitepress/config.ts
+++ b/docs/.vitepress/config.ts
@@ -1,13 +1,14 @@
import { defineConfig } from "vitepress";
+// Detect current version/branch from environment variable for multi-version support
+const base = process.env.VITEPRESS_BASE || "/express-cli/";
+
export default defineConfig({
title: "Express Forge",
description: "โก Production-ready Express backends in seconds",
+ base: base,
- // Important for GitHub Pages deployment
- base: "/express-cli/",
-
- head: [["link", { rel: "icon", href: "/express-cli/logo.svg" }]],
+ head: [["link", { rel: "icon", href: `${base}logo.svg` }]],
themeConfig: {
logo: "/logo.svg",
@@ -19,6 +20,14 @@ export default defineConfig({
nav: [
{ text: "Guide", link: "/guide/getting-started" },
{ text: "Features", link: "/guide/features" },
+ {
+ text: "Versions",
+ items: [
+ { text: "Latest (v2.x)", link: "https://code-y02.github.io/express-cli/" },
+ { text: "Beta (Next)", link: "https://code-y02.github.io/express-cli/next/" },
+ { text: "Legacy (v1.x)", link: "https://code-y02.github.io/express-cli/v1/" },
+ ],
+ },
{ text: "Reference", link: "/reference/cli-options" },
],
@@ -28,7 +37,6 @@ export default defineConfig({
items: [
{ text: "What is Express Forge?", link: "/" },
{ text: "Getting Started", link: "/guide/getting-started" },
- { text: "Roadmap", link: "/roadmap" },
],
},
{
@@ -37,6 +45,9 @@ export default defineConfig({
{ text: "Architecture Patterns", link: "/guide/architecture" },
{ text: "Project Structure", link: "/guide/structure" },
{ text: "Core Features", link: "/guide/features" },
+ { text: "Authentication", link: "/guide/auth" },
+ { text: "Caching", link: "/guide/caching" },
+ { text: "API Documentation", link: "/guide/openapi" },
],
},
{
diff --git a/docs/.vitepress/theme/custom.css b/docs/.vitepress/theme/custom.css
index 8c329ba..16348f1 100644
--- a/docs/.vitepress/theme/custom.css
+++ b/docs/.vitepress/theme/custom.css
@@ -121,8 +121,14 @@ h1, h2, h3, h4, h5, h6 {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
+/* Hero Buttons */
+.VPHero .actions {
+ justify-content: center !important;
+ gap: 12px !important;
+}
+
/* Overflow & Clipping Prevention */
-* {
+.vp-doc p, .vp-doc li {
overflow-wrap: break-word;
}
diff --git a/docs/guide/auth.md b/docs/guide/auth.md
new file mode 100644
index 0000000..8c4a955
--- /dev/null
+++ b/docs/guide/auth.md
@@ -0,0 +1,43 @@
+# Authentication
+
+Express Forge provides two battle-tested authentication strategies: **JWT** (JSON Web Tokens) and **Sessions**.
+
+## Strategies
+
+### ๐ JWT (Stateless)
+The modern standard for web APIs. Highly scalable and perfect for mobile apps and SPAs.
+- **Middleware**: `src/middlewares/auth.middleware.ts`
+- **Config**: Set `JWT_SECRET` in your `.env`.
+
+### ๐ช Session (Stateful)
+Traditional cookie-based authentication. Excellent for server-side rendered apps or when you need built-in session management.
+- **Middleware**: `src/middlewares/auth.middleware.ts`
+- **Config**: Set `SESSION_SECRET` in your `.env`.
+
+## Using the Auth Middleware
+
+To protect a route, simply add the `auth` middleware to your route definition.
+
+```typescript
+import { auth } from '../middlewares/auth.middleware.js';
+import { userController } from '../controllers/user.controller.js';
+
+// Protected route
+router.get('/profile', auth, userController.getProfile);
+```
+
+## Accessing the User
+
+Once a user is authenticated, their information is attached to the `req.user` object (for JWT) or `req.session.user` (for Sessions).
+
+```typescript
+export const getProfile = (req: Request, res: Response) => {
+ const user = req.user; // For JWT
+ return ApiResponse.success(res, user);
+};
+```
+
+## Security Best Practices
+1. **Secret Management**: Never commit your `JWT_SECRET` or `SESSION_SECRET` to version control. Use `.env` files.
+2. **HTTPS**: Always serve your API over HTTPS in production to protect tokens and session cookies.
+3. **HTTP-Only Cookies**: If using sessions, Express Forge pre-configures cookies to be `httpOnly` to prevent XSS attacks.
diff --git a/docs/guide/caching.md b/docs/guide/caching.md
new file mode 100644
index 0000000..098518e
--- /dev/null
+++ b/docs/guide/caching.md
@@ -0,0 +1,50 @@
+# Caching
+
+Express Forge provides a flexible caching layer to boost your API performance and reduce database load. You can choose between **Redis** (distributed) or **Node-Cache** (in-memory) during the scaffolding process.
+
+## Supported Drivers
+
+### ๐ด Redis
+Recommended for production environments and distributed systems where multiple server instances need to share a cache.
+- **Requirement**: A running Redis instance.
+- **Config**: Set `REDIS_URL` in your `.env` file.
+
+### ๐พ Node-Cache
+An in-memory caching solution that requires zero external dependencies. Perfect for simple applications or single-server setups.
+- **Requirement**: None.
+- **Config**: Automatic.
+
+## Usage
+
+The caching logic is encapsulated in `src/cache/index.ts`. It provides a unified interface regardless of the driver you chose.
+
+### Getting a Value
+```typescript
+import { cache } from '../cache/index.js';
+
+const user = await cache.get('user:123');
+if (user) {
+ return JSON.parse(user);
+}
+```
+
+### Setting a Value
+You can optionally set a Time-To-Live (TTL) in seconds.
+```typescript
+import { cache } from '../cache/index.js';
+
+// Cache for 1 hour (3600 seconds)
+await cache.set('user:123', JSON.stringify(userData), 3600);
+```
+
+### Deleting a Value
+```typescript
+import { cache } from '../cache/index.js';
+
+await cache.del('user:123');
+```
+
+## Best Practices
+1. **Cache Invalidation**: Always delete or update the cache when the underlying data in the database changes.
+2. **Serialization**: Since Redis only stores strings, ensure you `JSON.stringify()` your objects before setting and `JSON.parse()` when getting.
+3. **Fail-Safe**: Express Forge handles Redis connection errors gracefully via the logger, preventing your entire app from crashing if the cache is down.
diff --git a/docs/guide/openapi.md b/docs/guide/openapi.md
new file mode 100644
index 0000000..a0d91f3
--- /dev/null
+++ b/docs/guide/openapi.md
@@ -0,0 +1,43 @@
+# API Documentation (OpenAPI)
+
+Express Forge integrates **Swagger UI** to provide interactive, live documentation for your API. This allows your frontend team or external partners to test endpoints directly from the browser.
+
+## Getting Started
+
+If you enabled OpenAPI during scaffolding, your documentation is available at:
+`http://localhost:3000/docs`
+
+## Configuration
+
+The documentation configuration is located in `src/docs/swagger.ts`. It uses `swagger-jsdoc` to parse JSDoc comments in your route files.
+
+## Documenting Endpoints
+
+To add an endpoint to the Swagger UI, simply add a `@openapi` or `@swagger` block above your route definition.
+
+### Example
+
+```typescript
+/**
+ * @openapi
+ * /users:
+ * get:
+ * summary: Retrieve a list of users
+ * description: Returns a list of users from the database.
+ * responses:
+ * 200:
+ * description: A list of users.
+ * content:
+ * application/json:
+ * schema:
+ * type: array
+ * items:
+ * $ref: '#/components/schemas/User'
+ */
+router.get('/', userController.getAllUsers);
+```
+
+## Benefits
+- **Live Testing**: Use the "Try it out" button to make real requests to your development server.
+- **Auto-Sync**: Your documentation lives next to your code, making it easier to keep it updated.
+- **Standardized**: Uses the OpenAPI 3.0 specification, compatible with many other tools (Postman, Insomnia, etc.).
diff --git a/docs/index.md b/docs/index.md
index b433813..48b4109 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -15,12 +15,6 @@ hero:
- theme: alt
text: Documentation
link: /guide/architecture
- - theme: alt
- text: GitHub
- link: https://github.com/CODE-Y02/express-cli
- - theme: alt
- text: ๐ Roadmap
- link: /roadmap
features:
- icon: โก
@@ -35,18 +29,23 @@ features:
- icon: ๐พ
title: Type-safe ORM
details: First-class support for Prisma and Sequelize with pre-configured migrations.
- - icon: ๐ณ
- title: Docker Ready
- details: Multi-stage Dockerfiles and docker-compose setups included for seamless deployment.
+ - icon: ๐
+ title: Auth Scaffolding
+ details: Pre-configured JWT or Session-based authentication with best-practice security.
+ - icon: ๐
+ title: OpenAPI Docs
+ details: Automatically generated Swagger documentation from your Zod schemas and routes.
+ - icon: ๐ด
+ title: Redis Caching
+ details: Built-in support for Redis and Node-Cache to boost your API performance.
- icon: ๐งช
title: Quality Assured
details: Pre-configured testing suites with Vitest or Jest, including example integration tests.
- - icon: ๐ก๏ธ
- title: Enterprise Logic
- details: Centralized error handling, custom ApiError class, and structured ApiResponse wrappers.
+ - icon: ๐ณ
+ title: Docker Ready
+ details: Multi-stage Dockerfiles and docker-compose setups included for seamless deployment.
---
-
### ๐ Key Benefits
@@ -106,21 +105,6 @@ features:
transform: translateY(-2px);
}
-.why-section h2 {
- font-size: 2rem;
- font-weight: 800;
- margin-bottom: 24px;
- background: var(--vp-home-hero-name-background);
- -webkit-background-clip: text;
- -webkit-text-fill-color: transparent;
-}
-
- .why-section p {
- font-size: 1.1rem;
- color: var(--vp-c-text-2);
- line-height: 1.6;
-}
-
.why-section h3 {
margin-top: 48px;
font-size: 1.5rem;
@@ -150,14 +134,6 @@ features:
border-color: var(--vp-c-brand-1);
}
-.why-section li strong {
- display: block;
- margin-bottom: 8px;
- color: var(--vp-c-brand-1);
- font-size: 1.1rem;
- font-weight: 700;
-}
-
@media (max-width: 768px) {
.why-section h2 {
font-size: 2rem;
diff --git a/package.json b/package.json
index c78b9c1..31d0e6e 100644
--- a/package.json
+++ b/package.json
@@ -13,14 +13,16 @@
"release": "pnpm run build && pnpm run test && changeset publish",
"docs:dev": "cd docs && pnpm run dev",
"docs:build": "cd docs && pnpm run build",
- "docs:preview": "cd docs && pnpm run preview"
+ "docs:preview": "cd docs && pnpm run preview",
+ "prepare": "husky"
},
"devDependencies": {
"@changesets/cli": "^2.27.7",
+ "eslint": "^8.57.0",
+ "husky": "^9.1.7",
"prettier": "^3.3.3",
"turbo": "^2.0.9",
- "typescript": "5.5.3",
- "eslint": "^8.57.0"
+ "typescript": "5.5.3"
},
"packageManager": "pnpm@9.0.0",
"engines": {
diff --git a/packages/create-express-forge/CHANGELOG.md b/packages/create-express-forge/CHANGELOG.md
index 164b4a5..7dd6622 100644
--- a/packages/create-express-forge/CHANGELOG.md
+++ b/packages/create-express-forge/CHANGELOG.md
@@ -1,5 +1,11 @@
# Changelog
+## 2.0.0
+
+### Major Changes
+
+- 3b31da8: v2
+
## 1.0.0
### Major Changes
diff --git a/packages/create-express-forge/package.json b/packages/create-express-forge/package.json
index f6e35da..6b37b30 100644
--- a/packages/create-express-forge/package.json
+++ b/packages/create-express-forge/package.json
@@ -1,6 +1,6 @@
{
"name": "create-express-forge",
- "version": "1.0.0",
+ "version": "2.0.0",
"description": "โก Scaffold production-ready Express.js TypeScript backends in seconds",
"license": "MIT",
"type": "module",
diff --git a/packages/create-express-forge/src/generator/features/auth.ts b/packages/create-express-forge/src/generator/features/auth.ts
new file mode 100644
index 0000000..3faed16
--- /dev/null
+++ b/packages/create-express-forge/src/generator/features/auth.ts
@@ -0,0 +1,51 @@
+import path from 'path';
+import fs from 'fs-extra';
+import type { CliOptions } from '../../types.js';
+
+export async function generateAuth(opts: CliOptions, targetDir: string): Promise {
+ if (opts.auth === 'none') return;
+
+ const authDir = path.join(targetDir, 'src/middlewares');
+ await fs.ensureDir(authDir);
+
+ if (opts.auth === 'jwt') {
+ await fs.writeFile(
+ path.join(authDir, 'auth.middleware.ts'),
+ `import type { Request, Response, NextFunction } from 'express';
+import jwt from 'jsonwebtoken';
+import { ApiError } from '../utils/ApiError.js';
+import { env } from '../config/env.js';
+
+export const auth = (req: Request, _res: Response, next: NextFunction) => {
+ const authHeader = req.headers.authorization;
+ if (!authHeader?.startsWith('Bearer ')) {
+ return next(ApiError.unauthorized('No token provided'));
+ }
+
+ const token = authHeader.split(' ')[1];
+ try {
+ const decoded = jwt.verify(token, process.env.JWT_SECRET || 'secret');
+ req.user = decoded as any;
+ next();
+ } catch (err) {
+ next(ApiError.unauthorized('Invalid or expired token'));
+ }
+};
+`
+ );
+ } else if (opts.auth === 'session') {
+ await fs.writeFile(
+ path.join(authDir, 'auth.middleware.ts'),
+ `import type { Request, Response, NextFunction } from 'express';
+import { ApiError } from '../utils/ApiError.js';
+
+export const auth = (req: Request, _res: Response, next: NextFunction) => {
+ if (!req.session || !(req.session as any).user) {
+ return next(ApiError.unauthorized('Session expired or invalid'));
+ }
+ next();
+};
+`
+ );
+ }
+}
diff --git a/packages/create-express-forge/src/generator/features/cache.ts b/packages/create-express-forge/src/generator/features/cache.ts
new file mode 100644
index 0000000..9be3b85
--- /dev/null
+++ b/packages/create-express-forge/src/generator/features/cache.ts
@@ -0,0 +1,67 @@
+import path from 'path';
+import fs from 'fs-extra';
+import type { CliOptions } from '../../types.js';
+
+export async function generateCache(opts: CliOptions, targetDir: string): Promise {
+ if (opts.cache === 'none') return;
+
+ const cacheDir = path.join(targetDir, 'src/cache');
+ await fs.ensureDir(cacheDir);
+
+ if (opts.cache === 'redis') {
+ await fs.writeFile(
+ path.join(cacheDir, 'index.ts'),
+ `import { createClient } from 'redis';
+import { logger } from '../logger/index.js';
+
+const client = createClient({
+ url: process.env.REDIS_URL || 'redis://localhost:6379'
+});
+
+client.on('error', (err) => logger.error('Redis Client Error', err));
+
+export const connectRedis = async () => {
+ await client.connect();
+ logger.info('๐ด Redis connected successfully');
+};
+
+export const cache = {
+ get: async (key: string) => client.get(key),
+ set: async (key: string, value: string, ttlSeconds?: number) => {
+ if (ttlSeconds) {
+ await client.set(key, value, { EX: ttlSeconds });
+ } else {
+ await client.set(key, value);
+ }
+ },
+ del: async (key: string) => client.del(key),
+};
+`
+ );
+ } else if (opts.cache === 'node-cache') {
+ await fs.writeFile(
+ path.join(cacheDir, 'index.ts'),
+ `import NodeCache from 'node-cache';
+import { logger } from '../logger/index.js';
+
+const nodeCache = new NodeCache({ stdTTL: 100, checkperiod: 120 });
+
+logger.info('๐พ In-memory cache initialized');
+
+export const cache = {
+ get: async (key: string) => nodeCache.get(key) as string | undefined,
+ set: async (key: string, value: string, ttlSeconds?: number) => {
+ if (ttlSeconds) {
+ nodeCache.set(key, value, ttlSeconds);
+ } else {
+ nodeCache.set(key, value);
+ }
+ },
+ del: async (key: string) => {
+ nodeCache.del(key);
+ },
+};
+`
+ );
+ }
+}
diff --git a/packages/create-express-forge/src/generator/features/openapi.ts b/packages/create-express-forge/src/generator/features/openapi.ts
new file mode 100644
index 0000000..9fabe51
--- /dev/null
+++ b/packages/create-express-forge/src/generator/features/openapi.ts
@@ -0,0 +1,43 @@
+import path from 'path';
+import fs from 'fs-extra';
+import type { CliOptions } from '../../types.js';
+
+export async function generateOpenApi(opts: CliOptions, targetDir: string): Promise {
+ if (!opts.openapi) return;
+
+ const docsDir = path.join(targetDir, 'src/docs');
+ await fs.ensureDir(docsDir);
+
+ await fs.writeFile(
+ path.join(docsDir, 'swagger.ts'),
+ `import swaggerJsdoc from 'swagger-jsdoc';
+import swaggerUi from 'swagger-ui-express';
+import type { Express } from 'express';
+
+const options: swaggerJsdoc.Options = {
+ definition: {
+ openapi: '3.0.0',
+ info: {
+ title: '${opts.projectName} API',
+ version: '1.0.0',
+ description: 'API documentation for ${opts.projectName}',
+ },
+ servers: [
+ {
+ url: 'http://localhost:3000',
+ description: 'Development server',
+ },
+ ],
+ },
+ apis: ['./src/app.ts', './src/modules/**/*.routes.ts', './src/routes/**/*.ts'],
+};
+
+const specs = swaggerJsdoc(options);
+
+export const setupSwagger = (app: Express) => {
+ app.use('/docs', swaggerUi.serve, swaggerUi.setup(specs));
+ console.log('๐ API Docs available at http://localhost:3000/docs');
+};
+`
+ );
+}
diff --git a/packages/create-express-forge/src/generator/index.ts b/packages/create-express-forge/src/generator/index.ts
index 2c412b4..5beb34a 100644
--- a/packages/create-express-forge/src/generator/index.ts
+++ b/packages/create-express-forge/src/generator/index.ts
@@ -15,6 +15,9 @@ import { generateSequelize } from './features/sequelize.js';
import { generateLogger } from './features/logger.js';
import { generateTesting } from './features/testing.js';
import { generateDocker } from './features/docker.js';
+import { generateAuth } from './features/auth.js';
+import { generateCache } from './features/cache.js';
+import { generateOpenApi } from './features/openapi.js';
export async function generateProject(opts: CliOptions, targetDir: string): Promise {
const spinner = ora();
@@ -75,6 +78,24 @@ export async function generateProject(opts: CliOptions, targetDir: string): Prom
spinner.succeed(chalk.green('Docker + docker-compose'));
}
+ if (opts.auth !== 'none') {
+ spinner.start(chalk.dim(`Configuring ${opts.auth} authentication...`));
+ await generateAuth(opts, targetDir);
+ spinner.succeed(chalk.green(`${opts.auth.toUpperCase()} Auth`));
+ }
+
+ if (opts.cache !== 'none') {
+ spinner.start(chalk.dim(`Configuring ${opts.cache} caching...`));
+ await generateCache(opts, targetDir);
+ spinner.succeed(chalk.green(`${opts.cache.toUpperCase()} Cache`));
+ }
+
+ if (opts.openapi) {
+ spinner.start(chalk.dim('Generating OpenAPI docs...'));
+ await generateOpenApi(opts, targetDir);
+ spinner.succeed(chalk.green('OpenAPI (Swagger)'));
+ }
+
if (opts.installDeps) {
spinner.start(chalk.dim('Installing dependencies...'));
await execa('npm', ['install'], { cwd: targetDir });
diff --git a/packages/create-express-forge/src/prompts.ts b/packages/create-express-forge/src/prompts.ts
index 58b75e1..548baf5 100644
--- a/packages/create-express-forge/src/prompts.ts
+++ b/packages/create-express-forge/src/prompts.ts
@@ -66,6 +66,26 @@ export async function runCLI(initialProjectName?: string) {
],
});
+ const auth = await select({
+ message: 'Authentication strategy:',
+ choices: [
+ { name: '๐ JWT (stateless, token-based โ recommended)', value: 'jwt' },
+ { name: '๐ช Session (stateful, cookie-based)', value: 'session' },
+ { name: 'โฌ None', value: 'none' },
+ ],
+ });
+
+ const cache = await select({
+ message: 'Caching layer:',
+ choices: [
+ { name: '๐ด Redis (distributed, high-performance)', value: 'redis' },
+ { name: '๐พ Node-Cache (simple, in-memory)', value: 'node-cache' },
+ { name: 'โฌ None', value: 'none' },
+ ],
+ });
+
+ const openapi = await confirm({ message: 'Add OpenAPI (Swagger) documentation?', default: true });
+
const docker = await confirm({ message: 'Add Docker + docker-compose?', default: true });
const installDeps = await confirm({ message: 'Install dependencies now?', default: true });
@@ -76,6 +96,9 @@ export async function runCLI(initialProjectName?: string) {
database,
logger,
testing,
+ auth,
+ cache,
+ openapi,
docker,
installDeps,
};
diff --git a/packages/create-express-forge/src/types.ts b/packages/create-express-forge/src/types.ts
index 03b6431..401cda1 100644
--- a/packages/create-express-forge/src/types.ts
+++ b/packages/create-express-forge/src/types.ts
@@ -3,6 +3,8 @@ export type ORM = 'prisma' | 'sequelize' | 'none';
export type Database = 'postgresql' | 'mysql' | 'sqlite' | 'none';
export type LoggerLib = 'winston' | 'pino' | 'none';
export type TestingLib = 'vitest' | 'jest' | 'none';
+export type CacheLib = 'redis' | 'node-cache' | 'none';
+export type AuthStrategy = 'jwt' | 'session' | 'none';
export interface CliOptions {
projectName: string;
@@ -11,6 +13,9 @@ export interface CliOptions {
database: Database;
logger: LoggerLib;
testing: TestingLib;
+ cache: CacheLib;
+ auth: AuthStrategy;
+ openapi: boolean;
docker: boolean;
installDeps: boolean;
}
diff --git a/packages/create-express-forge/src/utils/package-builder.ts b/packages/create-express-forge/src/utils/package-builder.ts
index aa8a552..6a7703c 100644
--- a/packages/create-express-forge/src/utils/package-builder.ts
+++ b/packages/create-express-forge/src/utils/package-builder.ts
@@ -1,7 +1,7 @@
import type { CliOptions } from '../types.js';
export function buildPackageJson(opts: CliOptions): object {
- const { projectName, orm, database, logger, testing } = opts;
+ const { projectName, orm, database, logger, testing, cache, auth, openapi } = opts;
const deps: Record = {
express: '^4.19.2',
@@ -58,6 +58,27 @@ export function buildPackageJson(opts: CliOptions): object {
devDeps['@types/supertest'] = '^6.0.2';
}
+ if (cache === 'redis') {
+ deps['redis'] = '^4.6.14';
+ } else if (cache === 'node-cache') {
+ deps['node-cache'] = '^5.1.2';
+ }
+
+ if (auth === 'jwt') {
+ deps['jsonwebtoken'] = '^9.0.2';
+ devDeps['@types/jsonwebtoken'] = '^9.0.6';
+ } else if (auth === 'session') {
+ deps['express-session'] = '^1.18.0';
+ devDeps['@types/express-session'] = '^1.18.0';
+ }
+
+ if (openapi) {
+ deps['swagger-jsdoc'] = '^6.2.8';
+ deps['swagger-ui-express'] = '^5.0.1';
+ devDeps['@types/swagger-jsdoc'] = '^6.0.4';
+ devDeps['@types/swagger-ui-express'] = '^4.1.6';
+ }
+
const scripts: Record = {
dev: 'tsx watch src/server.ts',
build: 'tsup src/server.ts --format esm --dts',
diff --git a/packages/create-express-forge/tests/generator.test.ts b/packages/create-express-forge/tests/generator.test.ts
index 5ea3854..7024ec3 100644
--- a/packages/create-express-forge/tests/generator.test.ts
+++ b/packages/create-express-forge/tests/generator.test.ts
@@ -16,6 +16,9 @@ const baseOpts: CliOptions = {
database: 'none',
logger: 'none',
testing: 'none',
+ cache: 'none',
+ auth: 'none',
+ openapi: false,
docker: false,
installDeps: false,
};
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 5d21c24..3b9bf6d 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -14,6 +14,9 @@ importers:
eslint:
specifier: ^8.57.0
version: 8.57.1
+ husky:
+ specifier: ^9.1.7
+ version: 9.1.7
prettier:
specifier: ^3.3.3
version: 3.8.3
@@ -1615,6 +1618,11 @@ packages:
resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==}
engines: {node: '>=18.18.0'}
+ husky@9.1.7:
+ resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==}
+ engines: {node: '>=18'}
+ hasBin: true
+
iconv-lite@0.7.2:
resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==}
engines: {node: '>=0.10.0'}
@@ -4052,6 +4060,8 @@ snapshots:
human-signals@8.0.1: {}
+ husky@9.1.7: {}
+
iconv-lite@0.7.2:
dependencies:
safer-buffer: 2.1.2