From 2831289385ce37ce89dfdf672a6a62fa54fee7e7 Mon Sep 17 00:00:00 2001 From: Richard Zampieri Date: Mon, 29 Dec 2025 20:49:22 -0800 Subject: [PATCH 01/21] feat: initialize ExpressoTS application with basic configuration, routing, and testing setup - Added .env.example for environment variable configuration - Created .gitignore to exclude unnecessary files - Implemented expressots.config.ts for CLI scaffolding - Set up Jest configuration for testing - Added package.json with scripts and dependencies - Developed basic application structure with AppController and App class - Included README.md for project documentation - Established TypeScript configuration files for building and development --- application/.env.example | 15 +++ application/.gitignore | 30 +++++ application/README.md | 116 ++++++++++++++++++ application/expressots.config.ts | 51 ++++++++ application/jest.config.ts | 21 ++++ application/package.json | 38 ++++++ application/src/app.controller.ts | 31 +++++ application/src/app.ts | 31 +++++ application/src/config.ts | 51 ++++++++ application/src/main.ts | 5 + application/test/app.controller.spec.ts | 34 +++++ .../tsconfig.build.json | 6 +- .../tsconfig.json | 17 ++- micro/.env.local | 4 + micro/README.md | 105 +++++++++------- micro/package.json | 43 +++---- micro/src/api.ts | 37 +++++- non_opinionated/.eslintrc.js | 29 ----- non_opinionated/.gitignore | 36 ------ non_opinionated/.prettierrc | 6 - non_opinionated/README.md | 74 ----------- non_opinionated/expressots.config.ts | 10 -- non_opinionated/jest.config.ts | 14 --- non_opinionated/package.json | 41 ------- non_opinionated/src/app.controller.ts | 9 -- non_opinionated/src/app.ts | 20 --- non_opinionated/src/main.ts | 4 - non_opinionated/test/app.controller.spec.ts | 29 ----- non_opinionated/tsconfig.build.json | 22 ---- opinionated/.env.development | 2 - opinionated/.env.production | 2 - opinionated/.eslintrc.js | 28 ----- opinionated/.gitignore | 36 ------ opinionated/.prettierrc | 6 - opinionated/README.md | 74 ----------- opinionated/expressots.config.ts | 10 -- opinionated/jest.config.ts | 21 ---- opinionated/package.json | 42 ------- opinionated/register-path.js | 17 --- opinionated/src/app.ts | 33 ----- opinionated/src/env.ts | 11 -- opinionated/src/main.ts | 10 -- .../src/useCases/app/app.controller.ts | 13 -- opinionated/src/useCases/app/app.module.ts | 4 - opinionated/src/useCases/app/app.usecase.ts | 8 -- opinionated/test/app.controller.spec.ts | 29 ----- opinionated/tsconfig.json | 32 ----- 47 files changed, 556 insertions(+), 751 deletions(-) create mode 100644 application/.env.example create mode 100644 application/.gitignore create mode 100644 application/README.md create mode 100644 application/expressots.config.ts create mode 100644 application/jest.config.ts create mode 100644 application/package.json create mode 100644 application/src/app.controller.ts create mode 100644 application/src/app.ts create mode 100644 application/src/config.ts create mode 100644 application/src/main.ts create mode 100644 application/test/app.controller.spec.ts rename {opinionated => application}/tsconfig.build.json (80%) rename {non_opinionated => application}/tsconfig.json (67%) create mode 100644 micro/.env.local delete mode 100644 non_opinionated/.eslintrc.js delete mode 100644 non_opinionated/.gitignore delete mode 100644 non_opinionated/.prettierrc delete mode 100644 non_opinionated/README.md delete mode 100644 non_opinionated/expressots.config.ts delete mode 100644 non_opinionated/jest.config.ts delete mode 100644 non_opinionated/package.json delete mode 100644 non_opinionated/src/app.controller.ts delete mode 100644 non_opinionated/src/app.ts delete mode 100644 non_opinionated/src/main.ts delete mode 100644 non_opinionated/test/app.controller.spec.ts delete mode 100644 non_opinionated/tsconfig.build.json delete mode 100644 opinionated/.env.development delete mode 100644 opinionated/.env.production delete mode 100644 opinionated/.eslintrc.js delete mode 100644 opinionated/.gitignore delete mode 100644 opinionated/.prettierrc delete mode 100644 opinionated/README.md delete mode 100644 opinionated/expressots.config.ts delete mode 100644 opinionated/jest.config.ts delete mode 100644 opinionated/package.json delete mode 100644 opinionated/register-path.js delete mode 100644 opinionated/src/app.ts delete mode 100644 opinionated/src/env.ts delete mode 100644 opinionated/src/main.ts delete mode 100644 opinionated/src/useCases/app/app.controller.ts delete mode 100644 opinionated/src/useCases/app/app.module.ts delete mode 100644 opinionated/src/useCases/app/app.usecase.ts delete mode 100644 opinionated/test/app.controller.spec.ts delete mode 100644 opinionated/tsconfig.json diff --git a/application/.env.example b/application/.env.example new file mode 100644 index 0000000..ccc1992 --- /dev/null +++ b/application/.env.example @@ -0,0 +1,15 @@ +# Application Configuration +# Copy this file to .env.local for development + +# Application +APP_NAME="ExpressoTS App" +APP_VERSION="1.0.0" +NODE_ENV="development" + +# Server +PORT=3000 +HOST="localhost" +API_PREFIX="/api" + +# Logging +LOG_LEVEL="info" diff --git a/application/.gitignore b/application/.gitignore new file mode 100644 index 0000000..a58f85f --- /dev/null +++ b/application/.gitignore @@ -0,0 +1,30 @@ +# Dependencies +node_modules/ + +# Build output +dist/ + +# Environment files (except examples) +.env +.env.local +.env.staging +.env.prod +!.env.example + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Testing +coverage/ + +# Logs +*.log +npm-debug.log* + diff --git a/application/README.md b/application/README.md new file mode 100644 index 0000000..7d7625e --- /dev/null +++ b/application/README.md @@ -0,0 +1,116 @@ +# ExpressoTS Application + +A modern, type-safe Node.js backend powered by ExpressoTS v4.0. + +## Features + +- ๐Ÿ”ง **Type-Safe Configuration** - Full TypeScript inference for config values +- ๐Ÿš€ **Zero-Config Bootstrap** - Just run and go +- ๐Ÿ“ฆ **Build-Time Path Resolution** - No runtime overhead +- ๐Ÿงช **Testing Ready** - Jest configured out of the box + +## Quick Start + +```bash +# Install dependencies +npm install + +# Start development server +npm run dev + +# Build for production +npm run build + +# Run in production +npm run prod +``` + +## Project Structure + +``` +src/ +โ”œโ”€โ”€ main.ts # Application entry point +โ”œโ”€โ”€ app.ts # Application class (middleware, lifecycle) +โ”œโ”€โ”€ config.ts # Type-safe configuration +โ””โ”€โ”€ app.controller.ts # Example controller +``` + +## Configuration + +Configuration is managed through `src/config.ts` using the `defineConfig` helper: + +```typescript +export const appConfig = defineConfig({ + app: { + name: Env.string("APP_NAME").default("My App"), + version: Env.string("APP_VERSION").default("1.0.0"), + }, + server: { + port: Env.number("PORT").default(3000), + }, +}); +``` + +### Environment Files + +- `.env.local` - Development environment (default) +- `.env.staging` - Staging environment +- `.env.prod` - Production environment + +## Scaffolding + +Use the CLI to generate new resources: + +```bash +# Generate a controller +npx expressots generate controller user + +# Generate a use case +npx expressots generate usecase user/create-user + +# Generate a module with controller and use case +npx expressots generate module user +``` + +### Scaffold Configuration + +Customize scaffolding in `expressots.config.ts`: + +```typescript +const config: ExpressoConfig = { + opinionated: true, // Enable structured folders + scaffoldPattern: Pattern.KEBAB_CASE, + scaffoldSchematics: { + controller: "controllers", + usecase: "useCases", + entity: "entities", + }, +}; +``` + +## Testing + +```bash +# Run tests +npm test + +# Run tests with coverage +npm run test:cov + +# Watch mode +npm run test:watch +``` + +## API Endpoints + +| Method | Path | Description | +|--------|------|-------------| +| GET | /api/ | Application info | +| GET | /api/health | Health check | + +## Learn More + +- [ExpressoTS Documentation](https://expresso-ts.com) +- [GitHub Repository](https://github.com/expressots) +- [Discord Community](https://discord.gg/PyPJfGK) + diff --git a/application/expressots.config.ts b/application/expressots.config.ts new file mode 100644 index 0000000..ba3a636 --- /dev/null +++ b/application/expressots.config.ts @@ -0,0 +1,51 @@ +import { ExpressoConfig, Pattern } from "@expressots/shared"; + +/** + * ExpressoTS Configuration + * + * This file controls how the CLI scaffolds and manages your project. + * + * Options: + * - entryPoint: Main file name (without extension) + * - sourceRoot: Source code directory + * - scaffoldPattern: Naming convention (KEBAB_CASE, SNAKE_CASE, PASCAL_CASE, CAMEL_CASE) + * - opinionated: Enable structured folder scaffolding with path aliases + * - scaffoldSchematics: Customize folder/file names for scaffolding + */ +const config: ExpressoConfig = { + // Entry point for the application + entryPoint: "main", + + // Source directory + sourceRoot: "src", + + // File naming pattern for scaffolded resources + scaffoldPattern: Pattern.KEBAB_CASE, + + // Enable structured scaffolding with path aliases + // true: Resources go to folders like src/useCases/, src/entities/, etc. + // false: Resources go directly to src/ with flat structure + opinionated: true, + + // Customize scaffold folder names (optional) + // Uncomment and modify to use different folder names + // scaffoldSchematics: { + // // Core schematics + // entity: "entities", + // controller: "controllers", // Default: "useCases" + // usecase: "useCases", + // dto: "dto", + // module: "modules", + // provider: "providers", + // middleware: "middleware", + // // v4.0 schematics + // interceptor: "interceptors", + // event: "events", + // handler: "events", + // guard: "guards", + // config: "config", + // }, +}; + +export default config; + diff --git a/application/jest.config.ts b/application/jest.config.ts new file mode 100644 index 0000000..5f9b0e8 --- /dev/null +++ b/application/jest.config.ts @@ -0,0 +1,21 @@ +import type { Config } from "jest"; + +const config: Config = { + preset: "ts-jest", + testEnvironment: "node", + rootDir: ".", + testMatch: ["/test/**/*.spec.ts"], + moduleNameMapper: { + "^@app/(.*)$": "/src/$1", + }, + collectCoverageFrom: [ + "src/**/*.ts", + "!src/**/*.d.ts", + "!src/main.ts", + ], + coverageDirectory: "coverage", + coverageReporters: ["text", "lcov"], +}; + +export default config; + diff --git a/application/package.json b/application/package.json new file mode 100644 index 0000000..e448b65 --- /dev/null +++ b/application/package.json @@ -0,0 +1,38 @@ +{ + "name": "expressots-app", + "version": "1.0.0", + "description": "ExpressoTS Application", + "author": "", + "license": "MIT", + "main": "dist/src/main.js", + "scripts": { + "dev": "expressots dev", + "build": "expressots build", + "prod": "expressots prod", + "test": "jest --runInBand", + "test:watch": "jest --watchAll", + "test:cov": "jest --coverage", + "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", + "lint": "eslint \"src/**/*.ts\" --fix" + }, + "dependencies": { + "@expressots/adapter-express": "^4.0.0-beta.1", + "@expressots/core": "^4.0.0-beta.1", + "express": "^5.0.1", + "reflect-metadata": "^0.2.2" + }, + "devDependencies": { + "@expressots/cli": "^2.0.0", + "@expressots/shared": "^1.5.0", + "@types/express": "^5.0.0", + "@types/jest": "^29.5.14", + "@types/node": "^22.10.2", + "jest": "^29.7.0", + "nodemon": "^3.1.9", + "prettier": "^3.4.2", + "ts-jest": "^29.2.5", + "tsconfig-paths": "^4.2.0", + "tsx": "^4.19.2", + "typescript": "^5.7.2" + } +} diff --git a/application/src/app.controller.ts b/application/src/app.controller.ts new file mode 100644 index 0000000..01d6471 --- /dev/null +++ b/application/src/app.controller.ts @@ -0,0 +1,31 @@ +import { controller, Get } from "@expressots/adapter-express"; +import { config } from "./config"; + +interface AppInfo { + name: string; + version: string; + environment: string; + message: string; +} + +@controller("/") +export class AppController { + @Get("/") + execute(): AppInfo { + return { + name: config.app.name, + version: config.app.version, + environment: config.app.environment, + message: "Welcome to ExpressoTS!", + }; + } + + @Get("/health") + health(): { status: string; timestamp: string } { + return { + status: "healthy", + timestamp: new Date().toISOString(), + }; + } +} + diff --git a/application/src/app.ts b/application/src/app.ts new file mode 100644 index 0000000..0acbb1a --- /dev/null +++ b/application/src/app.ts @@ -0,0 +1,31 @@ +import { AppExpress } from "@expressots/adapter-express"; +import { AppContainer, CreateModule, Logger } from "@expressots/core"; +import { AppController } from "./app.controller"; +import { config } from "./config"; + +export class App extends AppExpress { + private container: AppContainer = this.configContainer([ + CreateModule([AppController]), + ]); + + async globalConfiguration(): Promise { + this.setGlobalRoutePrefix(config.server.globalPrefix); + } + + async configureServices(): Promise { + const logger = this.Provider.get(Logger); + logger.configure({ + level: config.logging.level.toUpperCase() as any, + }); + + // Add middleware + this.Middleware.addBodyParser(); + this.Middleware.setErrorHandler({ + showStackTrace: await this.isDevelopment(), + }); + } + + async postServerInitialization(): Promise {} + + async serverShutdown(): Promise {} +} diff --git a/application/src/config.ts b/application/src/config.ts new file mode 100644 index 0000000..cfe0853 --- /dev/null +++ b/application/src/config.ts @@ -0,0 +1,51 @@ +import { defineConfig, Env, loadEnvSync } from "@expressots/core"; + +/** + * Environment file mapping + * Maps environment names to their corresponding .env files + */ +const envFiles = { + development: ".env.local", + production: ".env.prod", +}; + +// Load environment variables before config resolution +loadEnvSync({ files: envFiles }); + +/** + * Application Configuration + * + * Type-safe configuration with environment variable support. + * All values are validated and typed at compile time. + */ +export const appConfig = defineConfig({ + // Application metadata + app: { + name: Env.string("APP_NAME", { default: "ExpressoTS App" }), + version: Env.string("APP_VERSION", { default: "1.0.0" }), + environment: Env.string("NODE_ENV", { default: "development" }), + }, + + // Server settings + server: { + port: Env.number("PORT", { default: 3000 }), + host: Env.string("HOST", { default: "localhost" }), + globalPrefix: Env.string("API_PREFIX", { default: "/api" }), + }, + + // Logging configuration + logging: { + level: Env.string("LOG_LEVEL", { default: "info" }), + }, + + // Bootstrap configuration (passed to bootstrap function) + bootstrap: { + envFileConfig: { + files: envFiles, + autoCreateTemplate: true, + }, + }, +}); + +// Export resolved configuration values +export const config = appConfig.values; diff --git a/application/src/main.ts b/application/src/main.ts new file mode 100644 index 0000000..d30fbd3 --- /dev/null +++ b/application/src/main.ts @@ -0,0 +1,5 @@ +import { bootstrap } from "@expressots/core"; +import { App } from "./app"; +import { config } from "./config"; + +bootstrap(App, config); diff --git a/application/test/app.controller.spec.ts b/application/test/app.controller.spec.ts new file mode 100644 index 0000000..0e97425 --- /dev/null +++ b/application/test/app.controller.spec.ts @@ -0,0 +1,34 @@ +import { describe, it, expect } from "@jest/globals"; +import { AppController } from "../src/app.controller"; + +describe("AppController", () => { + let controller: AppController; + + beforeEach(() => { + controller = new AppController(); + }); + + describe("execute", () => { + it("should return application info", () => { + const mockRes = {} as any; + const result = controller.execute(mockRes); + + expect(result).toBeDefined(); + expect(result.message).toBe("Welcome to ExpressoTS!"); + expect(result).toHaveProperty("name"); + expect(result).toHaveProperty("version"); + expect(result).toHaveProperty("environment"); + }); + }); + + describe("health", () => { + it("should return health status", () => { + const mockRes = {} as any; + const result = controller.health(mockRes); + + expect(result.status).toBe("healthy"); + expect(result).toHaveProperty("timestamp"); + }); + }); +}); + diff --git a/opinionated/tsconfig.build.json b/application/tsconfig.build.json similarity index 80% rename from opinionated/tsconfig.build.json rename to application/tsconfig.build.json index bed4d89..bb0f66e 100644 --- a/opinionated/tsconfig.build.json +++ b/application/tsconfig.build.json @@ -12,10 +12,7 @@ "rootDir": "./", "baseUrl": "./src", "paths": { - "@entities/*": ["entities/*"], - "@providers/*": ["providers/*"], - "@repositories/*": ["repositories/*"], - "@useCases/*": ["useCases/*"] + "@app/*": ["*"] }, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, @@ -29,3 +26,4 @@ "include": ["src/**/*.ts"], "exclude": ["node_modules", "dist", "test"] } + diff --git a/non_opinionated/tsconfig.json b/application/tsconfig.json similarity index 67% rename from non_opinionated/tsconfig.json rename to application/tsconfig.json index 981d0b7..509d4d8 100644 --- a/non_opinionated/tsconfig.json +++ b/application/tsconfig.json @@ -1,14 +1,20 @@ { "compilerOptions": { - "module": "commonjs", "target": "ES2021", + "module": "commonjs", "declaration": true, + "declarationMap": true, + "sourceMap": true, "removeComments": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, "resolveJsonModule": true, - "sourceMap": true, - "rootDir": "./src", + "outDir": "./dist", + "rootDir": "./", + "baseUrl": "./src", + "paths": { + "@app/*": ["*"] + }, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, @@ -17,6 +23,7 @@ "skipLibCheck": true, "types": ["node", "jest"] }, - "include": ["src/**/*.ts"], - "exclude": ["node_modules", "dist", "test/**/*"] + "include": ["src/**/*.ts", "test/**/*.ts"], + "exclude": ["node_modules", "dist"] } + diff --git a/micro/.env.local b/micro/.env.local new file mode 100644 index 0000000..c120e15 --- /dev/null +++ b/micro/.env.local @@ -0,0 +1,4 @@ +# Development Environment +APP_NAME=ExpressoTS Micro +APP_VERSION=1.0.0 +PORT=3001 diff --git a/micro/README.md b/micro/README.md index c09d790..29fd666 100644 --- a/micro/README.md +++ b/micro/README.md @@ -1,74 +1,91 @@ -# Expresso TS +# ExpressoTS Micro -A Typescript + [Node.js]("https://nodejs.org/en/") lightweight framework for quick building scalable, easy to read and maintain, server-side applications ๐Ÿš€ +A lightweight, minimal ExpressoTS microservice template. -## Philosophy +## Features -ExpressoTS is a TypeScript lightweight framework for building scalable, readable and maintainable server-side applications. The framework provides a level of abstraction on top of common HTTP server framework Expressjs exposing their API's directly to the developer. This provides freedom and brings to the developer a tool that is well known and easy to use. +- ๐Ÿš€ **Minimal Footprint** - Just the essentials for microservices +- ๐Ÿ”ง **Type-Safe Config** - Environment variables with full TypeScript support +- โšก **Fast Startup** - Optimized for serverless and containers +- ๐Ÿงช **Testing Ready** - Jest configured out of the box -## How to use - -### Executing in development mode +## Quick Start ```bash -npm run dev -``` +# Install dependencies +npm install -### Generating production build +# Start development server +npm run dev -```bash +# Build for production npm run build -``` -### Executing in production mode - -```bash +# Run in production npm run prod ``` -## Test +## Project Structure -How to run test scripts - -### Unit tests - -```bash -npm run test +``` +src/ +โ””โ”€โ”€ api.ts # Single file containing all routes and config ``` -### Test coverage +## Configuration -```bash -npm run test:cov +The micro template uses inline configuration with `defineConfig`: + +```typescript +const config = defineConfig({ + app: { + name: Env.string("APP_NAME", { default: "ExpressoTS Micro" }), + }, + server: { + port: Env.number("PORT", { default: 3000 }), + }, +}); ``` -## Documentation +## API Endpoints -- Here is our [Official Documentation](https://expresso-ts.com/) -- Checkout our [First Steps documentation](https://expresso-ts.com/docs/overview/first-steps) -- Our [CLI Documentation](https://expresso-ts.com/docs/category/cli) +| Method | Path | Description | +|--------|------|-------------| +| GET | / | Service info | +| GET | /health | Health check | -## Questions +## Adding Routes -For questions and support please use the Official [Discord Channel](https://discord.com/invite/PyPJfGK). We have a very active community there, that will be happy to help you. Post your questions in the channel called **HELP EXPRESSO TS** and forum called **help**. +```typescript +app.Route.get("/users", (req, res) => { + res.json({ users: [] }); +}); -## Issues +app.Route.post("/users", (req, res) => { + const user = req.body; + res.status(201).json(user); +}); +``` -The [Issue Reporting Channel](https://github.com/expressots/expressots/issues) is for bug report and feature request **only**. +## Environment Variables -Before you create an issue, please make sure you read the [Contribution Guidelines](CONTRIBUTING.md). +Create a `.env.local` file for development: -## Support the project +```env +APP_NAME="My Microservice" +APP_VERSION="1.0.0" +PORT=3000 +``` -Expresso TS is an MIT-licensed open source project. It's an independent project with ongoing development made possible thanks to your support. If you'd like to help, please consider: +## Use Cases -- Become a sponsor on **[Sponsor no GitHub](https://github.com/sponsors/expressots)** -- Follow the **[organization](https://github.com/expressots)** on GitHub and Star โญ the project -- Subscribe to the Twitch channel: **[Richard Zampieri](https://www.twitch.tv/richardzampieri)** -- Join our **[Discord](https://discord.com/invite/PyPJfGK)** -- Contribute submitting **[issues and pull requests](https://github.com/expressots/expressots/issues/new/choose)** -- Share the project with your friends and colleagues +- Microservices in a distributed system +- Serverless functions (AWS Lambda, Vercel) +- Lightweight APIs +- Proof of concept / prototypes -## License +## Learn More -ExpressoTS is **[MIT licensed](LICENSE.md)** \ No newline at end of file +- [ExpressoTS Documentation](https://expresso-ts.com) +- [GitHub Repository](https://github.com/expressots) +- [Discord Community](https://discord.gg/PyPJfGK) diff --git a/micro/package.json b/micro/package.json index 87b907e..3e40551 100644 --- a/micro/package.json +++ b/micro/package.json @@ -1,41 +1,38 @@ { - "name": "micro", + "name": "expressots-micro", "version": "1.0.0", - "description": "", + "description": "ExpressoTS Microservice", "author": "", - "private": true, - "license": "UNLICENSED", + "license": "MIT", + "main": "dist/src/api.js", "scripts": { "build": "expressots build", "dev": "expressots dev", "prod": "expressots prod", - "test": "jest", + "test": "jest --runInBand", "test:watch": "jest --watchAll", "test:cov": "jest --coverage", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "lint": "eslint \"src/**/*.ts\" --fix" }, - "keywords": [], "dependencies": { - "@expressots/adapter-express": "3.0.0", - "@expressots/core": "3.0.0", - "@expressots/shared": "3.0.0" + "@expressots/adapter-express": "file:../../adapter-express", + "@expressots/core": "file:../../expressots/expressots-core-4.0.0-beta.1.tgz", + "express": "^5.2.1", + "reflect-metadata": "^0.2.2" }, "devDependencies": { - "@expressots/cli": "3.0.0", - "@types/express": "5.0.0", - "@types/jest": "29.5.14", - "@types/node": "20.12.7", - "@typescript-eslint/eslint-plugin": "8.0.0", - "@typescript-eslint/parser": "8.0.0", - "eslint": "8.57.0", - "eslint-config-prettier": "9.0.0", - "eslint-plugin-prettier": "5.0.0", - "jest": "29.7.0", - "prettier": "3.2.5", + "@expressots/cli": "file:../../expressots-cli", + "@expressots/shared": "file:../../shared", + "@types/express": "^5.0.0", + "@types/jest": "^29.5.14", + "@types/node": "^22.10.2", + "jest": "^29.7.0", + "nodemon": "^3.1.11", + "prettier": "^3.4.2", "supertest": "^7.0.0", - "ts-jest": "29.1.2", - "tsx": "4.19.2", - "typescript": "5.1.3" + "ts-jest": "^29.2.5", + "tsx": "^4.19.2", + "typescript": "^5.7.2" } } diff --git a/micro/src/api.ts b/micro/src/api.ts index 57f6655..5da085e 100644 --- a/micro/src/api.ts +++ b/micro/src/api.ts @@ -1,12 +1,43 @@ import { createMicroAPI } from "@expressots/adapter-express"; +import { defineConfig, Env, loadEnvSync } from "@expressots/core"; import { Request, Response } from "express"; -const microAPI = createMicroAPI(); +// Environment configuration +loadEnvSync({ files: { development: ".env.local", production: ".env.prod" } }); + +// Type-safe configuration +const config = defineConfig({ + app: { + name: Env.string("APP_NAME", { default: "ExpressoTS Micro" }), + version: Env.string("APP_VERSION", { default: "1.0.0" }), + }, + server: { + port: Env.number("PORT", { default: 3000 }), + }, +}); +// Create micro API +const microAPI = createMicroAPI(); const app = microAPI.build(); +// Routes app.Route.get("/", (req: Request, res: Response) => { - res.send("Hello from ExpressoTS Micro API!"); + res.json({ + name: config.values.app.name, + version: config.values.app.version, + message: "Hello from ExpressoTS Micro API!", + }); }); -app.listen(3000); +app.Route.get("/health", (req: Request, res: Response) => { + res.json({ + status: "healthy", + timestamp: new Date().toISOString(), + }); +}); + +// Start server +const port = config.values.server.port; +app.listen(port, () => { + console.log(`๐Ÿš€ Micro API running on http://localhost:${port}`); +}); diff --git a/non_opinionated/.eslintrc.js b/non_opinionated/.eslintrc.js deleted file mode 100644 index 9340aff..0000000 --- a/non_opinionated/.eslintrc.js +++ /dev/null @@ -1,29 +0,0 @@ -module.exports = { - parser: "@typescript-eslint/parser", - plugins: ["@typescript-eslint/eslint-plugin"], - extends: [ - "plugin:@typescript-eslint/recommended", - "plugin:prettier/recommended", - ], - root: true, - env: { - node: true, - jest: true, - }, - ignorePatterns: [".eslintrc.js"], - rules: { - "@typescript-eslint/interface-name-prefix": "off", - "@typescript-eslint/explicit-function-return-type": "off", - "@typescript-eslint/explicit-module-boundary-types": "off", - "@typescript-eslint/no-inferrable-types": "off", - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-empty-function": "off", - "@typescript-eslint/no-empty-interface": "off", - "@typescript-eslint/no-unused-vars": "off", - "no-trailing-spaces": ["error", { skipBlankLines: true }], - "no-multi-spaces": ["error", { ignoreEOLComments: true }], - "no-multi-spaces": "off", - "prettier/prettier": ["error", { endOfLine: "auto" }, { tabWidth: 4 }], - "prettier/prettier": "off", - }, -}; diff --git a/non_opinionated/.gitignore b/non_opinionated/.gitignore deleted file mode 100644 index 94709eb..0000000 --- a/non_opinionated/.gitignore +++ /dev/null @@ -1,36 +0,0 @@ -# compiled output -/dist -/node_modules -.env - -# Logs -logs -*.log -npm-debug.log* -pnpm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* - -# OS -.DS_Store - -# Tests -/coverage -/.nyc_output - -# IDEs and editors -/.idea -.project -.classpath -.c9/ -*.launch -.settings/ -*.sublime-workspace - -# IDE - VSCode -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json \ No newline at end of file diff --git a/non_opinionated/.prettierrc b/non_opinionated/.prettierrc deleted file mode 100644 index d7708b4..0000000 --- a/non_opinionated/.prettierrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "singleQuote": false, - "trailingComma": "all", - "endOfLine": "auto", - "tabWidth": 4 -} \ No newline at end of file diff --git a/non_opinionated/README.md b/non_opinionated/README.md deleted file mode 100644 index c09d790..0000000 --- a/non_opinionated/README.md +++ /dev/null @@ -1,74 +0,0 @@ -# Expresso TS - -A Typescript + [Node.js]("https://nodejs.org/en/") lightweight framework for quick building scalable, easy to read and maintain, server-side applications ๐Ÿš€ - -## Philosophy - -ExpressoTS is a TypeScript lightweight framework for building scalable, readable and maintainable server-side applications. The framework provides a level of abstraction on top of common HTTP server framework Expressjs exposing their API's directly to the developer. This provides freedom and brings to the developer a tool that is well known and easy to use. - -## How to use - -### Executing in development mode - -```bash -npm run dev -``` - -### Generating production build - -```bash -npm run build -``` - -### Executing in production mode - -```bash -npm run prod -``` - -## Test - -How to run test scripts - -### Unit tests - -```bash -npm run test -``` - -### Test coverage - -```bash -npm run test:cov -``` - -## Documentation - -- Here is our [Official Documentation](https://expresso-ts.com/) -- Checkout our [First Steps documentation](https://expresso-ts.com/docs/overview/first-steps) -- Our [CLI Documentation](https://expresso-ts.com/docs/category/cli) - -## Questions - -For questions and support please use the Official [Discord Channel](https://discord.com/invite/PyPJfGK). We have a very active community there, that will be happy to help you. Post your questions in the channel called **HELP EXPRESSO TS** and forum called **help**. - -## Issues - -The [Issue Reporting Channel](https://github.com/expressots/expressots/issues) is for bug report and feature request **only**. - -Before you create an issue, please make sure you read the [Contribution Guidelines](CONTRIBUTING.md). - -## Support the project - -Expresso TS is an MIT-licensed open source project. It's an independent project with ongoing development made possible thanks to your support. If you'd like to help, please consider: - -- Become a sponsor on **[Sponsor no GitHub](https://github.com/sponsors/expressots)** -- Follow the **[organization](https://github.com/expressots)** on GitHub and Star โญ the project -- Subscribe to the Twitch channel: **[Richard Zampieri](https://www.twitch.tv/richardzampieri)** -- Join our **[Discord](https://discord.com/invite/PyPJfGK)** -- Contribute submitting **[issues and pull requests](https://github.com/expressots/expressots/issues/new/choose)** -- Share the project with your friends and colleagues - -## License - -ExpressoTS is **[MIT licensed](LICENSE.md)** \ No newline at end of file diff --git a/non_opinionated/expressots.config.ts b/non_opinionated/expressots.config.ts deleted file mode 100644 index cc58e55..0000000 --- a/non_opinionated/expressots.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { ExpressoConfig, Pattern } from "@expressots/shared"; - -const config: ExpressoConfig = { - entryPoint: "main", - sourceRoot: "src", - scaffoldPattern: Pattern.KEBAB_CASE, - opinionated: false -}; - -export default config; \ No newline at end of file diff --git a/non_opinionated/jest.config.ts b/non_opinionated/jest.config.ts deleted file mode 100644 index cebb7e5..0000000 --- a/non_opinionated/jest.config.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { JestConfigWithTsJest } from "ts-jest"; - -const jestConfig: JestConfigWithTsJest = { - preset: "ts-jest", - rootDir: "./", - testEnvironment: "node", - verbose: true, - automock: false, - testMatch: ["**/*.test.ts", "**/*.spec.ts"], - coverageDirectory: "./coverage", - coverageReporters: ["text", "html", "json"], -}; - -export default jestConfig; diff --git a/non_opinionated/package.json b/non_opinionated/package.json deleted file mode 100644 index c9eedda..0000000 --- a/non_opinionated/package.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "name": "nopinionated", - "version": "1.0.0", - "description": "", - "author": "", - "private": true, - "license": "UNLICENSED", - "scripts": { - "build": "expressots build", - "dev": "expressots dev", - "prod": "expressots prod", - "test": "jest", - "test:watch": "jest --watchAll", - "test:cov": "jest --coverage", - "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", - "lint": "eslint \"src/**/*.ts\" --fix" - }, - "keywords": [], - "dependencies": { - "@expressots/adapter-express": "3.0.0", - "@expressots/core": "3.0.0", - "@expressots/shared": "3.0.0" - }, - "devDependencies": { - "@expressots/cli": "3.0.0", - "@types/express": "5.0.0", - "@types/jest": "29.5.14", - "@types/node": "20.12.7", - "@typescript-eslint/eslint-plugin": "8.0.0", - "@typescript-eslint/parser": "8.0.0", - "eslint": "8.57.0", - "eslint-config-prettier": "9.0.0", - "eslint-plugin-prettier": "5.0.0", - "jest": "29.7.0", - "prettier": "3.2.5", - "supertest": "^7.0.0", - "ts-jest": "29.1.2", - "tsx": "4.19.2", - "typescript": "5.1.3" - } -} diff --git a/non_opinionated/src/app.controller.ts b/non_opinionated/src/app.controller.ts deleted file mode 100644 index b63c908..0000000 --- a/non_opinionated/src/app.controller.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { controller, Get } from "@expressots/adapter-express"; - -@controller("/") -export class AppController { - @Get("/") - execute() { - return "Hello from ExpressoTS!"; - } -} diff --git a/non_opinionated/src/app.ts b/non_opinionated/src/app.ts deleted file mode 100644 index cff71a4..0000000 --- a/non_opinionated/src/app.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { AppExpress } from "@expressots/adapter-express"; -import { AppContainer, CreateModule } from "@expressots/core"; -import { AppController } from "./app.controller"; - -export class App extends AppExpress { - private config: AppContainer = this.configContainer([ - CreateModule([AppController]), - ]); - - async globalConfiguration(): Promise {} - - async configureServices(): Promise { - this.Middleware.addBodyParser(); - this.Middleware.setErrorHandler({ showStackTrace: true }); - } - - async postServerInitialization(): Promise {} - - async serverShutdown(): Promise {} -} \ No newline at end of file diff --git a/non_opinionated/src/main.ts b/non_opinionated/src/main.ts deleted file mode 100644 index 11413b3..0000000 --- a/non_opinionated/src/main.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { AppFactory } from "@expressots/core"; -import { App } from "./app"; - -AppFactory.create(App).then((app) => app.listen(3000)); \ No newline at end of file diff --git a/non_opinionated/test/app.controller.spec.ts b/non_opinionated/test/app.controller.spec.ts deleted file mode 100644 index 3c43fc3..0000000 --- a/non_opinionated/test/app.controller.spec.ts +++ /dev/null @@ -1,29 +0,0 @@ -import request from "supertest"; -import { Server } from "http"; - -import { AppFactory, StatusCode } from '@expressots/core'; -import { IWebServerBuilder } from '@expressots/shared'; -import { App } from "../src/app"; - - -describe("AppController", () => { - let server: Server; - let webServerBuilder: IWebServerBuilder - - beforeAll(async () => { - webServerBuilder = await AppFactory.create(App); - const app = await webServerBuilder.listen(3000); - server = await app.getHttpServer(); - }); - - afterAll(async () => { - await server.close(); - }); - - it("returns Hello Expresso TS!", async () => { - return request(server) - .get("/") - .expect(StatusCode.OK) - .expect("Hello from ExpressoTS!"); - }); -}); diff --git a/non_opinionated/tsconfig.build.json b/non_opinionated/tsconfig.build.json deleted file mode 100644 index 6f440dc..0000000 --- a/non_opinionated/tsconfig.build.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2021", - "module": "commonjs", - "outDir": "./dist", - "rootDir": "./src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "moduleResolution": "node", - "resolveJsonModule": true, - "removeComments": true, - "noImplicitAny": false, - "strictPropertyInitialization": false, - "types": ["node", "jest"], - "experimentalDecorators": true, - "emitDecoratorMetadata": true - }, - "include": ["src/**/*.ts"], - "exclude": ["node_modules", "dist", "test/**/*"] -} diff --git a/opinionated/.env.development b/opinionated/.env.development deleted file mode 100644 index ea93d3e..0000000 --- a/opinionated/.env.development +++ /dev/null @@ -1,2 +0,0 @@ -# This file is used to set environment variables for development environment -PORT=3000 \ No newline at end of file diff --git a/opinionated/.env.production b/opinionated/.env.production deleted file mode 100644 index 71733c8..0000000 --- a/opinionated/.env.production +++ /dev/null @@ -1,2 +0,0 @@ -# This file is used to set environment variables for production environment -PORT=4000 \ No newline at end of file diff --git a/opinionated/.eslintrc.js b/opinionated/.eslintrc.js deleted file mode 100644 index f335b8d..0000000 --- a/opinionated/.eslintrc.js +++ /dev/null @@ -1,28 +0,0 @@ -module.exports = { - parser: "@typescript-eslint/parser", - plugins: ["@typescript-eslint/eslint-plugin"], - extends: [ - "plugin:@typescript-eslint/recommended", - "plugin:prettier/recommended", - ], - root: true, - env: { - node: true, - jest: true, - }, - ignorePatterns: [".eslintrc.js"], - rules: { - "@typescript-eslint/interface-name-prefix": "off", - "@typescript-eslint/explicit-function-return-type": "off", - "@typescript-eslint/explicit-module-boundary-types": "off", - "@typescript-eslint/no-inferrable-types": "off", - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-empty-function": "off", - "@typescript-eslint/no-empty-interface": "off", - "@typescript-eslint/no-unused-vars": "off", - "no-trailing-spaces": ["error", { skipBlankLines: true }], - "no-multi-spaces": ["error", { ignoreEOLComments: true }], - "no-multi-spaces": "off", - "prettier/prettier": "off", - }, -}; diff --git a/opinionated/.gitignore b/opinionated/.gitignore deleted file mode 100644 index 94709eb..0000000 --- a/opinionated/.gitignore +++ /dev/null @@ -1,36 +0,0 @@ -# compiled output -/dist -/node_modules -.env - -# Logs -logs -*.log -npm-debug.log* -pnpm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* - -# OS -.DS_Store - -# Tests -/coverage -/.nyc_output - -# IDEs and editors -/.idea -.project -.classpath -.c9/ -*.launch -.settings/ -*.sublime-workspace - -# IDE - VSCode -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json \ No newline at end of file diff --git a/opinionated/.prettierrc b/opinionated/.prettierrc deleted file mode 100644 index d7708b4..0000000 --- a/opinionated/.prettierrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "singleQuote": false, - "trailingComma": "all", - "endOfLine": "auto", - "tabWidth": 4 -} \ No newline at end of file diff --git a/opinionated/README.md b/opinionated/README.md deleted file mode 100644 index c09d790..0000000 --- a/opinionated/README.md +++ /dev/null @@ -1,74 +0,0 @@ -# Expresso TS - -A Typescript + [Node.js]("https://nodejs.org/en/") lightweight framework for quick building scalable, easy to read and maintain, server-side applications ๐Ÿš€ - -## Philosophy - -ExpressoTS is a TypeScript lightweight framework for building scalable, readable and maintainable server-side applications. The framework provides a level of abstraction on top of common HTTP server framework Expressjs exposing their API's directly to the developer. This provides freedom and brings to the developer a tool that is well known and easy to use. - -## How to use - -### Executing in development mode - -```bash -npm run dev -``` - -### Generating production build - -```bash -npm run build -``` - -### Executing in production mode - -```bash -npm run prod -``` - -## Test - -How to run test scripts - -### Unit tests - -```bash -npm run test -``` - -### Test coverage - -```bash -npm run test:cov -``` - -## Documentation - -- Here is our [Official Documentation](https://expresso-ts.com/) -- Checkout our [First Steps documentation](https://expresso-ts.com/docs/overview/first-steps) -- Our [CLI Documentation](https://expresso-ts.com/docs/category/cli) - -## Questions - -For questions and support please use the Official [Discord Channel](https://discord.com/invite/PyPJfGK). We have a very active community there, that will be happy to help you. Post your questions in the channel called **HELP EXPRESSO TS** and forum called **help**. - -## Issues - -The [Issue Reporting Channel](https://github.com/expressots/expressots/issues) is for bug report and feature request **only**. - -Before you create an issue, please make sure you read the [Contribution Guidelines](CONTRIBUTING.md). - -## Support the project - -Expresso TS is an MIT-licensed open source project. It's an independent project with ongoing development made possible thanks to your support. If you'd like to help, please consider: - -- Become a sponsor on **[Sponsor no GitHub](https://github.com/sponsors/expressots)** -- Follow the **[organization](https://github.com/expressots)** on GitHub and Star โญ the project -- Subscribe to the Twitch channel: **[Richard Zampieri](https://www.twitch.tv/richardzampieri)** -- Join our **[Discord](https://discord.com/invite/PyPJfGK)** -- Contribute submitting **[issues and pull requests](https://github.com/expressots/expressots/issues/new/choose)** -- Share the project with your friends and colleagues - -## License - -ExpressoTS is **[MIT licensed](LICENSE.md)** \ No newline at end of file diff --git a/opinionated/expressots.config.ts b/opinionated/expressots.config.ts deleted file mode 100644 index fb06e5f..0000000 --- a/opinionated/expressots.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { ExpressoConfig, Pattern } from "@expressots/shared"; - -const config: ExpressoConfig = { - entryPoint: "main", - sourceRoot: "src", - scaffoldPattern: Pattern.KEBAB_CASE, - opinionated: true, -}; - -export default config; diff --git a/opinionated/jest.config.ts b/opinionated/jest.config.ts deleted file mode 100644 index bac9448..0000000 --- a/opinionated/jest.config.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { JestConfigWithTsJest } from "ts-jest"; - -const jestConfig: JestConfigWithTsJest = { - preset: "ts-jest", - rootDir: "./", - testEnvironment: "node", - verbose: true, - automock: false, - testMatch: ["**/*.test.ts", "**/*.spec.ts"], - coverageDirectory: "./coverage", - coverageReporters: ["text", "html", "json"], - moduleNameMapper: { - "^@entities/(.*)$": "/src/entities/$1", - "^@providers/(.*)$": "/src/providers/$1", - "^@repositories/(.*)$": "/src/repositories/$1", - "^@useCases/(.*)$": "/src/useCases/$1", - }, - modulePathIgnorePatterns: ["/dist/"], -}; - -export default jestConfig; diff --git a/opinionated/package.json b/opinionated/package.json deleted file mode 100644 index e21afa9..0000000 --- a/opinionated/package.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "name": "opinionated", - "version": "1.0.0", - "description": "", - "author": "", - "private": true, - "license": "UNLICENSED", - "scripts": { - "build": "expressots build", - "dev": "expressots dev", - "prod": "expressots prod", - "test": "jest", - "test:watch": "jest --watchAll", - "test:cov": "jest --coverage", - "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", - "lint": "eslint \"src/**/*.ts\" --fix" - }, - "keywords": [], - "dependencies": { - "@expressots/adapter-express": "3.0.0", - "@expressots/core": "3.0.0", - "@expressots/shared": "3.0.0" - }, - "devDependencies": { - "@expressots/cli": "3.0.0", - "@types/express": "5.0.0", - "@types/jest": "29.5.14", - "@types/node": "20.12.7", - "@typescript-eslint/eslint-plugin": "8.0.0", - "@typescript-eslint/parser": "8.0.0", - "eslint": "8.57.0", - "eslint-config-prettier": "9.0.0", - "eslint-plugin-prettier": "5.0.0", - "jest": "29.7.0", - "prettier": "3.2.5", - "supertest": "^7.0.0", - "ts-jest": "29.1.2", - "tsconfig-paths": "4.2.0", - "tsx": "4.19.2", - "typescript": "5.1.3" - } -} diff --git a/opinionated/register-path.js b/opinionated/register-path.js deleted file mode 100644 index 116ebb7..0000000 --- a/opinionated/register-path.js +++ /dev/null @@ -1,17 +0,0 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ - -const path = require("path"); -const tsconfigPaths = require("tsconfig-paths"); -const tsconfig = require("./tsconfig.build.json"); - -const baseUrl = tsconfig.compilerOptions.baseUrl || "."; -const outDir = tsconfig.compilerOptions.outDir || "."; - -let baseUrlPath = path.resolve(outDir, baseUrl); - -const explicitPaths = { - baseUrl: baseUrlPath, - paths: tsconfig.compilerOptions.paths, -}; - -tsconfigPaths.register(explicitPaths); diff --git a/opinionated/src/app.ts b/opinionated/src/app.ts deleted file mode 100644 index 343b6e8..0000000 --- a/opinionated/src/app.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { AppExpress } from "@expressots/adapter-express"; -import { AppContainer, Env } from "@expressots/core"; -import { AppModule } from "@useCases/app/app.module"; - -export class App extends AppExpress { - private config: AppContainer = this.configContainer([AppModule]); - - async globalConfiguration(): Promise { - this.setGlobalRoutePrefix("/v1"); - - this.initEnvironment("development", { - env: { - development: ".env.development", - production: ".env.production", - }, - }); - } - - async configureServices(): Promise { - this.Provider.register(Env); - - this.Middleware.addBodyParser(); - this.Middleware.setErrorHandler({ showStackTrace: true }); - } - - async postServerInitialization(): Promise { - if (await this.isDevelopment()) { - this.Provider.get(Env).checkFile(".env.development"); - } - } - - async serverShutdown(): Promise {} -} diff --git a/opinionated/src/env.ts b/opinionated/src/env.ts deleted file mode 100644 index a2d2d25..0000000 --- a/opinionated/src/env.ts +++ /dev/null @@ -1,11 +0,0 @@ -import pkg from "../package.json"; - -export const env = { - App: { - appName: pkg.name, - appVersion: pkg.version, - get Port() { - return process.env.PORT || 3000; - } - }, -}; \ No newline at end of file diff --git a/opinionated/src/main.ts b/opinionated/src/main.ts deleted file mode 100644 index 693468f..0000000 --- a/opinionated/src/main.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { AppFactory } from "@expressots/core"; -import { App } from "app"; -import { env } from "env"; - -AppFactory.create(App).then((app) => - app.listen(env.App.Port, { - appName: env.App.appName, - appVersion: env.App.appVersion, - }), -); diff --git a/opinionated/src/useCases/app/app.controller.ts b/opinionated/src/useCases/app/app.controller.ts deleted file mode 100644 index dab0c36..0000000 --- a/opinionated/src/useCases/app/app.controller.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { controller, Get } from "@expressots/adapter-express"; -import { inject } from "@expressots/core"; -import { AppUseCase } from "./app.usecase"; - -@controller("/") -export class AppController { - @inject(AppUseCase) private appUseCase: AppUseCase; - - @Get("/") - execute() { - return this.appUseCase.execute(); - } -} diff --git a/opinionated/src/useCases/app/app.module.ts b/opinionated/src/useCases/app/app.module.ts deleted file mode 100644 index e148a06..0000000 --- a/opinionated/src/useCases/app/app.module.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { ContainerModule, CreateModule } from "@expressots/core"; -import { AppController } from "./app.controller"; - -export const AppModule: ContainerModule = CreateModule([AppController]); \ No newline at end of file diff --git a/opinionated/src/useCases/app/app.usecase.ts b/opinionated/src/useCases/app/app.usecase.ts deleted file mode 100644 index c491abb..0000000 --- a/opinionated/src/useCases/app/app.usecase.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { provide } from "@expressots/core"; - -@provide(AppUseCase) -export class AppUseCase { - execute() { - return "Hello from ExpressoTS!"; - } -} diff --git a/opinionated/test/app.controller.spec.ts b/opinionated/test/app.controller.spec.ts deleted file mode 100644 index 41f829e..0000000 --- a/opinionated/test/app.controller.spec.ts +++ /dev/null @@ -1,29 +0,0 @@ -import request from "supertest"; - -import { AppFactory, StatusCode } from "@expressots/core"; -import { IWebServerBuilder } from "@expressots/shared"; - -import { Server } from "http"; -import { App } from "../src/app"; - -describe("AppController", () => { - let server: Server; - let webServerBuilder: IWebServerBuilder; - - beforeAll(async () => { - webServerBuilder = await AppFactory.create(App); - const app = await webServerBuilder.listen(3000); - server = await app.getHttpServer(); - }); - - afterAll(async () => { - await server.close(); - }); - - it("returns a valid AppResponse", async () => { - return request(server) - .get("/v1") - .expect(StatusCode.OK) - .expect("Hello from ExpressoTS!"); - }); -}); diff --git a/opinionated/tsconfig.json b/opinionated/tsconfig.json deleted file mode 100644 index 6e94d5e..0000000 --- a/opinionated/tsconfig.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2021", - "module": "commonjs", - "lib": ["es2021"], - "declaration": false, - "removeComments": true, - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "resolveJsonModule": true, - "sourceMap": false, - "outDir": "./dist", - "rootDir": "./", - "baseUrl": "./src", - "paths": { - "@entities/*": ["entities/*"], - "@providers/*": ["providers/*"], - "@repositories/*": ["repositories/*"], - "@useCases/*": ["useCases/*"] - }, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "strict": true, - "allowJs": true, - "noImplicitAny": false, - "strictPropertyInitialization": false, - "skipLibCheck": true, - "types": ["node", "jest"] - }, - "include": ["src/**/*.ts"], - "exclude": ["node_modules", "dist", "test/**/*.ts"] -} From 00c8474e2bc5692072e3ab222c111871098cd82a Mon Sep 17 00:00:00 2001 From: Richard Zampieri Date: Mon, 29 Dec 2025 21:07:22 -0800 Subject: [PATCH 02/21] feat: updated version to v4 beta 1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1f03d1d..f07964b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@expressots/templates", - "version": "3.0.0", + "version": "4.0.0-beta.1", "description": "Expressots templates", "author": "Richard Zampieri", "license": "MIT", From 59a2e5072532c240fae71bff0c6ef6f1cfeb8924 Mon Sep 17 00:00:00 2001 From: Richard Zampieri Date: Mon, 29 Dec 2025 21:16:14 -0800 Subject: [PATCH 03/21] refactor: update package.json dependencies and simplify AppController response --- application/package.json | 72 +++++++++++++++---------------- application/src/app.controller.ts | 26 +---------- 2 files changed, 38 insertions(+), 60 deletions(-) diff --git a/application/package.json b/application/package.json index e448b65..fc58aec 100644 --- a/application/package.json +++ b/application/package.json @@ -1,38 +1,38 @@ { - "name": "expressots-app", - "version": "1.0.0", - "description": "ExpressoTS Application", - "author": "", - "license": "MIT", - "main": "dist/src/main.js", - "scripts": { - "dev": "expressots dev", - "build": "expressots build", - "prod": "expressots prod", - "test": "jest --runInBand", - "test:watch": "jest --watchAll", - "test:cov": "jest --coverage", - "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", - "lint": "eslint \"src/**/*.ts\" --fix" - }, - "dependencies": { - "@expressots/adapter-express": "^4.0.0-beta.1", - "@expressots/core": "^4.0.0-beta.1", - "express": "^5.0.1", - "reflect-metadata": "^0.2.2" - }, - "devDependencies": { - "@expressots/cli": "^2.0.0", - "@expressots/shared": "^1.5.0", - "@types/express": "^5.0.0", - "@types/jest": "^29.5.14", - "@types/node": "^22.10.2", - "jest": "^29.7.0", - "nodemon": "^3.1.9", - "prettier": "^3.4.2", - "ts-jest": "^29.2.5", - "tsconfig-paths": "^4.2.0", - "tsx": "^4.19.2", - "typescript": "^5.7.2" - } + "name": "expressots-app", + "version": "1.0.0", + "description": "ExpressoTS Application", + "author": "", + "license": "MIT", + "main": "dist/src/main.js", + "scripts": { + "dev": "expressots dev", + "build": "expressots build", + "prod": "expressots prod", + "test": "jest --runInBand", + "test:watch": "jest --watchAll", + "test:cov": "jest --coverage", + "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", + "lint": "eslint \"src/**/*.ts\" --fix" + }, + "dependencies": { + "@expressots/adapter-express": "4.0.0-beta.1", + "@expressots/core": "4.0.0-beta.1", + "express": "5.2.1", + "reflect-metadata": "0.2.2" + }, + "devDependencies": { + "@expressots/cli": "4.0.0-beta.1", + "@expressots/shared": "4.0.0-beta.1", + "@types/express": "5.0.0", + "@types/jest": "29.5.14", + "@types/node": "22.10.2", + "jest": "^29.7.0", + "nodemon": "3.1.11", + "prettier": "^3.4.2", + "ts-jest": "29.2.5", + "tsconfig-paths": "^4.2.0", + "tsx": "4.19.2", + "typescript": "5.7.2" + } } diff --git a/application/src/app.controller.ts b/application/src/app.controller.ts index 01d6471..b9b53b2 100644 --- a/application/src/app.controller.ts +++ b/application/src/app.controller.ts @@ -1,31 +1,9 @@ import { controller, Get } from "@expressots/adapter-express"; -import { config } from "./config"; - -interface AppInfo { - name: string; - version: string; - environment: string; - message: string; -} @controller("/") export class AppController { @Get("/") - execute(): AppInfo { - return { - name: config.app.name, - version: config.app.version, - environment: config.app.environment, - message: "Welcome to ExpressoTS!", - }; - } - - @Get("/health") - health(): { status: string; timestamp: string } { - return { - status: "healthy", - timestamp: new Date().toISOString(), - }; + execute(): string { + return "Hello from ExpressoTS!"; } } - From 30f7d4743c71c9d44a0a74d1dcef99afa12e72d8 Mon Sep 17 00:00:00 2001 From: Richard Zampieri Date: Mon, 29 Dec 2025 22:13:40 -0800 Subject: [PATCH 04/21] refactor: remove unused configuration files and simplify application structure - Deleted .env.example and config.ts as they are no longer needed - Updated tsconfig.json to include expressots.config.ts - Simplified App class by removing logger configuration - Adjusted AppController method signature for consistency - Enhanced test setup for AppController with zero-config test app --- application/.env.example | 15 ------ application/src/app.controller.ts | 2 +- application/src/app.ts | 12 +---- application/src/config.ts | 51 ------------------ application/src/main.ts | 3 +- application/test/app.controller.spec.ts | 52 ++++++++++-------- application/tsconfig.json | 4 +- micro/test/api.spec.ts | 70 +++++++++++++++++++++---- 8 files changed, 95 insertions(+), 114 deletions(-) delete mode 100644 application/.env.example delete mode 100644 application/src/config.ts diff --git a/application/.env.example b/application/.env.example deleted file mode 100644 index ccc1992..0000000 --- a/application/.env.example +++ /dev/null @@ -1,15 +0,0 @@ -# Application Configuration -# Copy this file to .env.local for development - -# Application -APP_NAME="ExpressoTS App" -APP_VERSION="1.0.0" -NODE_ENV="development" - -# Server -PORT=3000 -HOST="localhost" -API_PREFIX="/api" - -# Logging -LOG_LEVEL="info" diff --git a/application/src/app.controller.ts b/application/src/app.controller.ts index b9b53b2..b63c908 100644 --- a/application/src/app.controller.ts +++ b/application/src/app.controller.ts @@ -3,7 +3,7 @@ import { controller, Get } from "@expressots/adapter-express"; @controller("/") export class AppController { @Get("/") - execute(): string { + execute() { return "Hello from ExpressoTS!"; } } diff --git a/application/src/app.ts b/application/src/app.ts index 0acbb1a..ca12233 100644 --- a/application/src/app.ts +++ b/application/src/app.ts @@ -1,23 +1,15 @@ import { AppExpress } from "@expressots/adapter-express"; -import { AppContainer, CreateModule, Logger } from "@expressots/core"; +import { AppContainer, CreateModule } from "@expressots/core"; import { AppController } from "./app.controller"; -import { config } from "./config"; export class App extends AppExpress { private container: AppContainer = this.configContainer([ CreateModule([AppController]), ]); - async globalConfiguration(): Promise { - this.setGlobalRoutePrefix(config.server.globalPrefix); - } + async globalConfiguration(): Promise {} async configureServices(): Promise { - const logger = this.Provider.get(Logger); - logger.configure({ - level: config.logging.level.toUpperCase() as any, - }); - // Add middleware this.Middleware.addBodyParser(); this.Middleware.setErrorHandler({ diff --git a/application/src/config.ts b/application/src/config.ts deleted file mode 100644 index cfe0853..0000000 --- a/application/src/config.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { defineConfig, Env, loadEnvSync } from "@expressots/core"; - -/** - * Environment file mapping - * Maps environment names to their corresponding .env files - */ -const envFiles = { - development: ".env.local", - production: ".env.prod", -}; - -// Load environment variables before config resolution -loadEnvSync({ files: envFiles }); - -/** - * Application Configuration - * - * Type-safe configuration with environment variable support. - * All values are validated and typed at compile time. - */ -export const appConfig = defineConfig({ - // Application metadata - app: { - name: Env.string("APP_NAME", { default: "ExpressoTS App" }), - version: Env.string("APP_VERSION", { default: "1.0.0" }), - environment: Env.string("NODE_ENV", { default: "development" }), - }, - - // Server settings - server: { - port: Env.number("PORT", { default: 3000 }), - host: Env.string("HOST", { default: "localhost" }), - globalPrefix: Env.string("API_PREFIX", { default: "/api" }), - }, - - // Logging configuration - logging: { - level: Env.string("LOG_LEVEL", { default: "info" }), - }, - - // Bootstrap configuration (passed to bootstrap function) - bootstrap: { - envFileConfig: { - files: envFiles, - autoCreateTemplate: true, - }, - }, -}); - -// Export resolved configuration values -export const config = appConfig.values; diff --git a/application/src/main.ts b/application/src/main.ts index d30fbd3..8236042 100644 --- a/application/src/main.ts +++ b/application/src/main.ts @@ -1,5 +1,4 @@ import { bootstrap } from "@expressots/core"; import { App } from "./app"; -import { config } from "./config"; -bootstrap(App, config); +bootstrap(App); diff --git a/application/test/app.controller.spec.ts b/application/test/app.controller.spec.ts index 0e97425..b2affd0 100644 --- a/application/test/app.controller.spec.ts +++ b/application/test/app.controller.spec.ts @@ -1,34 +1,40 @@ -import { describe, it, expect } from "@jest/globals"; -import { AppController } from "../src/app.controller"; +import "reflect-metadata"; -describe("AppController", () => { - let controller: AppController; +import { + createTestApp, + setupExpressoTSMatchers, + TestAppResult, +} from "@expressots/core"; +import { afterAll, beforeAll, describe, it } from "@jest/globals"; +import { App } from "../src/app"; - beforeEach(() => { - controller = new AppController(); - }); +// Setup ExpressoTS custom matchers +setupExpressoTSMatchers(); - describe("execute", () => { - it("should return application info", () => { - const mockRes = {} as any; - const result = controller.execute(mockRes); +describe("AppController", () => { + let testApp: TestAppResult; - expect(result).toBeDefined(); - expect(result.message).toBe("Welcome to ExpressoTS!"); - expect(result).toHaveProperty("name"); - expect(result).toHaveProperty("version"); - expect(result).toHaveProperty("environment"); + beforeAll(async () => { + // Zero-config test app creation + testApp = await createTestApp(App, { + env: { + NODE_ENV: "test", + }, }); }); - describe("health", () => { - it("should return health status", () => { - const mockRes = {} as any; - const result = controller.health(mockRes); + afterAll(async () => { + // Clean up test app + await testApp.cleanup(); + }); - expect(result.status).toBe("healthy"); - expect(result).toHaveProperty("timestamp"); + describe("GET /", () => { + it("should return welcome message", async () => { + const response = await testApp.request + .get("/") + .expectStatus(200) + .expectBody("Hello from ExpressoTS!") + .execute(); }); }); }); - diff --git a/application/tsconfig.json b/application/tsconfig.json index 509d4d8..6930585 100644 --- a/application/tsconfig.json +++ b/application/tsconfig.json @@ -2,6 +2,7 @@ "compilerOptions": { "target": "ES2021", "module": "commonjs", + "moduleResolution": "node", "declaration": true, "declarationMap": true, "sourceMap": true, @@ -23,7 +24,6 @@ "skipLibCheck": true, "types": ["node", "jest"] }, - "include": ["src/**/*.ts", "test/**/*.ts"], + "include": ["src/**/*.ts", "test/**/*.ts", "expressots.config.ts"], "exclude": ["node_modules", "dist"] } - diff --git a/micro/test/api.spec.ts b/micro/test/api.spec.ts index 81e938e..2b5897c 100644 --- a/micro/test/api.spec.ts +++ b/micro/test/api.spec.ts @@ -1,26 +1,76 @@ +import "reflect-metadata"; import { createMicroAPI } from "@expressots/adapter-express"; import { Server } from "http"; -import request from "supertest"; +import { createFluentRequest } from "@expressots/core"; -describe("MicroAPI Root Route", () => { +describe("MicroAPI", () => { let httpServer: Server; + let baseUrl: string; - beforeAll(() => { + beforeAll(async () => { const microAPI = createMicroAPI(); const app = microAPI.build(); - app.listen(0); + // Add routes for testing + app.Route.get("/", (req, res) => { + res.json({ + message: "Hello from ExpressoTS Micro!", + version: "4.0.0", + }); + }); + + app.Route.get("/health", (req, res) => { + res.json({ + status: "healthy", + timestamp: new Date().toISOString(), + }); + }); + + // Listen on random port + await app.listen(0); httpServer = microAPI.getHttpServer(); + const address = httpServer.address() as { port: number }; + baseUrl = `http://localhost:${address.port}`; }); afterAll(() => { - httpServer.close(); + httpServer?.close(); + }); + + describe("GET /", () => { + it("should return welcome message", async () => { + const request = createFluentRequest(baseUrl); + const response = await request + .get("/") + .expectStatus(200) + .execute(); + + expect(response.body).toHaveProperty("message"); + expect(response.body.message).toBe("Hello from ExpressoTS Micro!"); + }); + }); + + describe("GET /health", () => { + it("should return health status", async () => { + const request = createFluentRequest(baseUrl); + const response = await request + .get("/health") + .expectStatus(200) + .execute(); + + expect(response.body.status).toBe("healthy"); + expect(response.body).toHaveProperty("timestamp"); + }); }); - it("should return Hello from ExpressoTS!", () => { - request(httpServer) - .get("/") - .expect(200) - .expect("Hello from ExpressoTS!"); + describe("Performance", () => { + it("should respond quickly", async () => { + const request = createFluentRequest(baseUrl); + await request + .get("/") + .expectStatus(200) + .expectTime({ lessThan: 50 }) + .execute(); + }); }); }); From 002341894e3ea25d032cff99774a79d6fe5a92ca Mon Sep 17 00:00:00 2001 From: Richard Zampieri Date: Mon, 29 Dec 2025 23:40:00 -0800 Subject: [PATCH 05/21] chore: update configuration files and improve TypeScript settings - Added *.tsbuildinfo to .gitignore to exclude TypeScript build info files - Simplified expressots.config.ts by removing unnecessary comments - Updated jest.config.ts to include --detectOpenHandles for better test handling - Modified package.json scripts for improved test and coverage commands - Enhanced tsconfig files with incremental builds and better type definitions - Cleaned up app.ts and app.controller.spec.ts by removing commented code --- application/.gitignore | 4 +-- application/expressots.config.ts | 41 ------------------------- application/jest.config.ts | 8 ++--- application/package.json | 40 +++++++++++++----------- application/src/app.ts | 1 - application/test/app.controller.spec.ts | 7 ++--- application/tsconfig.build.json | 28 +++++------------ application/tsconfig.json | 30 +++++++++++++++--- 8 files changed, 61 insertions(+), 98 deletions(-) diff --git a/application/.gitignore b/application/.gitignore index a58f85f..d6b5299 100644 --- a/application/.gitignore +++ b/application/.gitignore @@ -3,6 +3,7 @@ node_modules/ # Build output dist/ +*.tsbuildinfo # Environment files (except examples) .env @@ -26,5 +27,4 @@ coverage/ # Logs *.log -npm-debug.log* - +npm-debug.log* \ No newline at end of file diff --git a/application/expressots.config.ts b/application/expressots.config.ts index ba3a636..fb06e5f 100644 --- a/application/expressots.config.ts +++ b/application/expressots.config.ts @@ -1,51 +1,10 @@ import { ExpressoConfig, Pattern } from "@expressots/shared"; -/** - * ExpressoTS Configuration - * - * This file controls how the CLI scaffolds and manages your project. - * - * Options: - * - entryPoint: Main file name (without extension) - * - sourceRoot: Source code directory - * - scaffoldPattern: Naming convention (KEBAB_CASE, SNAKE_CASE, PASCAL_CASE, CAMEL_CASE) - * - opinionated: Enable structured folder scaffolding with path aliases - * - scaffoldSchematics: Customize folder/file names for scaffolding - */ const config: ExpressoConfig = { - // Entry point for the application entryPoint: "main", - - // Source directory sourceRoot: "src", - - // File naming pattern for scaffolded resources scaffoldPattern: Pattern.KEBAB_CASE, - - // Enable structured scaffolding with path aliases - // true: Resources go to folders like src/useCases/, src/entities/, etc. - // false: Resources go directly to src/ with flat structure opinionated: true, - - // Customize scaffold folder names (optional) - // Uncomment and modify to use different folder names - // scaffoldSchematics: { - // // Core schematics - // entity: "entities", - // controller: "controllers", // Default: "useCases" - // usecase: "useCases", - // dto: "dto", - // module: "modules", - // provider: "providers", - // middleware: "middleware", - // // v4.0 schematics - // interceptor: "interceptors", - // event: "events", - // handler: "events", - // guard: "guards", - // config: "config", - // }, }; export default config; - diff --git a/application/jest.config.ts b/application/jest.config.ts index 5f9b0e8..251492b 100644 --- a/application/jest.config.ts +++ b/application/jest.config.ts @@ -8,14 +8,10 @@ const config: Config = { moduleNameMapper: { "^@app/(.*)$": "/src/$1", }, - collectCoverageFrom: [ - "src/**/*.ts", - "!src/**/*.d.ts", - "!src/main.ts", - ], + modulePathIgnorePatterns: ["/dist"], + collectCoverageFrom: ["src/**/*.ts", "!src/**/*.d.ts", "!src/main.ts"], coverageDirectory: "coverage", coverageReporters: ["text", "lcov"], }; export default config; - diff --git a/application/package.json b/application/package.json index fc58aec..5d28b2e 100644 --- a/application/package.json +++ b/application/package.json @@ -9,30 +9,34 @@ "dev": "expressots dev", "build": "expressots build", "prod": "expressots prod", - "test": "jest --runInBand", + "test": "jest --runInBand --detectOpenHandles", "test:watch": "jest --watchAll", - "test:cov": "jest --coverage", + "coverage": "jest --coverage --detectOpenHandles", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", - "lint": "eslint \"src/**/*.ts\" --fix" + "lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\" --fix" }, "dependencies": { - "@expressots/adapter-express": "4.0.0-beta.1", - "@expressots/core": "4.0.0-beta.1", - "express": "5.2.1", - "reflect-metadata": "0.2.2" + "@expressots/adapter-express": "file:../adapter-express/expressots-adapter-express-4.0.0-beta.1.tgz", + "@expressots/core": "file:../expressots/expressots-core-4.0.0-beta.1.tgz", + "@expressots/shared": "file:../shared/expressots-shared-4.0.0-beta.1.tgz", + "express": "5.2.1" }, "devDependencies": { - "@expressots/cli": "4.0.0-beta.1", - "@expressots/shared": "4.0.0-beta.1", - "@types/express": "5.0.0", - "@types/jest": "29.5.14", - "@types/node": "22.10.2", - "jest": "^29.7.0", + "@eslint/js": "9.39.2", + "@expressots/cli": "file:../expressots-cli/expressots-cli-4.0.0-beta.1.tgz", + "@types/express": "5.0.6", + "@types/jest": "30.0.0", + "@types/node": "25.0.3", + "@typescript-eslint/eslint-plugin": "8.51.0", + "@typescript-eslint/parser": "8.51.0", + "eslint": "9.39.2", + "jest": "30.0.0", "nodemon": "3.1.11", - "prettier": "^3.4.2", - "ts-jest": "29.2.5", - "tsconfig-paths": "^4.2.0", - "tsx": "4.19.2", - "typescript": "5.7.2" + "prettier": "3.7.4", + "ts-jest": "29.4.6", + "tsconfig-paths": "4.2.0", + "tsx": "4.21.0", + "typescript": "5.9.3", + "typescript-eslint": "8.51.0" } } diff --git a/application/src/app.ts b/application/src/app.ts index ca12233..6f827a3 100644 --- a/application/src/app.ts +++ b/application/src/app.ts @@ -10,7 +10,6 @@ export class App extends AppExpress { async globalConfiguration(): Promise {} async configureServices(): Promise { - // Add middleware this.Middleware.addBodyParser(); this.Middleware.setErrorHandler({ showStackTrace: await this.isDevelopment(), diff --git a/application/test/app.controller.spec.ts b/application/test/app.controller.spec.ts index b2affd0..dcd7894 100644 --- a/application/test/app.controller.spec.ts +++ b/application/test/app.controller.spec.ts @@ -1,5 +1,3 @@ -import "reflect-metadata"; - import { createTestApp, setupExpressoTSMatchers, @@ -15,22 +13,21 @@ describe("AppController", () => { let testApp: TestAppResult; beforeAll(async () => { - // Zero-config test app creation testApp = await createTestApp(App, { env: { NODE_ENV: "test", }, + autoCleanup: false, }); }); afterAll(async () => { - // Clean up test app await testApp.cleanup(); }); describe("GET /", () => { it("should return welcome message", async () => { - const response = await testApp.request + await testApp.request .get("/") .expectStatus(200) .expectBody("Hello from ExpressoTS!") diff --git a/application/tsconfig.build.json b/application/tsconfig.build.json index bb0f66e..7c634db 100644 --- a/application/tsconfig.build.json +++ b/application/tsconfig.build.json @@ -1,29 +1,17 @@ { + "extends": "./tsconfig.json", "compilerOptions": { - "target": "ES2021", - "module": "commonjs", + "outDir": "./dist", + "rootDir": "./", "declaration": false, + "declarationMap": false, "sourceMap": false, "removeComments": true, - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "resolveJsonModule": true, - "outDir": "./dist", - "rootDir": "./", - "baseUrl": "./src", - "paths": { - "@app/*": ["*"] - }, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "strict": true, - "allowJs": true, - "noImplicitAny": false, - "strictPropertyInitialization": false, - "skipLibCheck": true, - "types": ["node", "jest"] + "incremental": true, + "tsBuildInfoFile": "./dist/.tsbuildinfo", + "isolatedModules": true, + "types": ["node"] }, "include": ["src/**/*.ts"], "exclude": ["node_modules", "dist", "test"] } - diff --git a/application/tsconfig.json b/application/tsconfig.json index 6930585..c8ef954 100644 --- a/application/tsconfig.json +++ b/application/tsconfig.json @@ -1,27 +1,47 @@ { "compilerOptions": { + // Language and Environment "target": "ES2021", + "lib": ["ES2021"], + + // Modules "module": "commonjs", "moduleResolution": "node", + "esModuleInterop": true, + "resolveJsonModule": true, + + // Emit + "outDir": "./dist", + "rootDir": "./", "declaration": true, "declarationMap": true, "sourceMap": true, "removeComments": true, + + // ExpressoTS Required: Decorators and Metadata "experimentalDecorators": true, "emitDecoratorMetadata": true, - "resolveJsonModule": true, - "outDir": "./dist", - "rootDir": "./", + + // Path Mapping "baseUrl": "./src", "paths": { "@app/*": ["*"] }, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, + + // Type Checking "strict": true, "noImplicitAny": false, "strictPropertyInitialization": false, "skipLibCheck": true, + + // Performance + "incremental": true, + "tsBuildInfoFile": "./dist/.tsbuildinfo", + + // Interop Constraints + "forceConsistentCasingInFileNames": true, + + // Type Definitions "types": ["node", "jest"] }, "include": ["src/**/*.ts", "test/**/*.ts", "expressots.config.ts"], From 3f007b13a8d54570bfde19fe5e9074ec2d932428 Mon Sep 17 00:00:00 2001 From: Richard Zampieri Date: Mon, 29 Dec 2025 23:42:09 -0800 Subject: [PATCH 06/21] feat: added eslint config --- application/eslint.config.mjs | 42 +++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 application/eslint.config.mjs diff --git a/application/eslint.config.mjs b/application/eslint.config.mjs new file mode 100644 index 0000000..2227ace --- /dev/null +++ b/application/eslint.config.mjs @@ -0,0 +1,42 @@ +import js from "@eslint/js"; +import tseslint from "typescript-eslint"; +import { fileURLToPath } from "node:url"; + +const __dirname = fileURLToPath(new URL(".", import.meta.url)); + +export default tseslint.config( + js.configs.recommended, + ...tseslint.configs.recommended, + { + files: ["src/**/*.ts", "test/**/*.ts"], + languageOptions: { + parserOptions: { + project: true, + tsconfigRootDir: __dirname, + }, + }, + rules: { + // TypeScript-specific rules + "@typescript-eslint/no-unused-vars": [ + "error", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + }, + ], + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-non-null-assertion": "warn", + }, + }, + { + ignores: [ + "node_modules/**", + "dist/**", + "coverage/**", + "*.config.ts", + "*.config.js", + "*.config.mjs", + ], + } +); From ce3553ffdb97bb45acef3b5756c52ac649a36196 Mon Sep 17 00:00:00 2001 From: Richard Zampieri Date: Tue, 30 Dec 2025 00:14:05 -0800 Subject: [PATCH 07/21] chore: simplify test and coverage scripts in package.json --- application/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/application/package.json b/application/package.json index 5d28b2e..725e7b3 100644 --- a/application/package.json +++ b/application/package.json @@ -9,9 +9,9 @@ "dev": "expressots dev", "build": "expressots build", "prod": "expressots prod", - "test": "jest --runInBand --detectOpenHandles", + "test": "jest --runInBand", "test:watch": "jest --watchAll", - "coverage": "jest --coverage --detectOpenHandles", + "coverage": "jest --coverage", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\" --fix" }, From 8ce30dc02334064c20a538db4b5e5480065e8725 Mon Sep 17 00:00:00 2001 From: Richard Zampieri Date: Tue, 30 Dec 2025 22:58:49 -0800 Subject: [PATCH 08/21] chore: update TypeScript configuration and ESLint settings - Added path mappings for useCases and controllers in tsconfig.build.json and tsconfig.json - Disabled the no-unused-vars rule in ESLint configuration for better flexibility --- application/eslint.config.mjs | 1 + application/tsconfig.build.json | 7 ++++++- application/tsconfig.json | 3 ++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/application/eslint.config.mjs b/application/eslint.config.mjs index 2227ace..33c6935 100644 --- a/application/eslint.config.mjs +++ b/application/eslint.config.mjs @@ -26,6 +26,7 @@ export default tseslint.config( ], "@typescript-eslint/explicit-function-return-type": "off", "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-unused-vars": "off", "@typescript-eslint/no-non-null-assertion": "warn", }, }, diff --git a/application/tsconfig.build.json b/application/tsconfig.build.json index 7c634db..4864d83 100644 --- a/application/tsconfig.build.json +++ b/application/tsconfig.build.json @@ -10,7 +10,12 @@ "incremental": true, "tsBuildInfoFile": "./dist/.tsbuildinfo", "isolatedModules": true, - "types": ["node"] + "types": ["node"], + "baseUrl": ".", + "paths": { + "@useCases/*": ["./src/useCases/*"], + "@controllers/*": ["./src/controllers/*"] + } }, "include": ["src/**/*.ts"], "exclude": ["node_modules", "dist", "test"] diff --git a/application/tsconfig.json b/application/tsconfig.json index c8ef954..a921762 100644 --- a/application/tsconfig.json +++ b/application/tsconfig.json @@ -25,7 +25,8 @@ // Path Mapping "baseUrl": "./src", "paths": { - "@app/*": ["*"] + "@useCases/*": ["./useCases/*"], + "@controllers/*": ["./controllers/*"] }, // Type Checking From e0269363d6346d4eabd76eafda213f3da6bfc5cc Mon Sep 17 00:00:00 2001 From: Richard Zampieri Date: Sun, 4 Jan 2026 23:05:38 -0800 Subject: [PATCH 09/21] refactor: changed globalConfiguration method from async to synchronous for improved performance and clarity --- application/src/app.ts | 2 +- commitlint.config.ts | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/application/src/app.ts b/application/src/app.ts index 6f827a3..90f4c7d 100644 --- a/application/src/app.ts +++ b/application/src/app.ts @@ -7,7 +7,7 @@ export class App extends AppExpress { CreateModule([AppController]), ]); - async globalConfiguration(): Promise {} + globalConfiguration(): void {} async configureServices(): Promise { this.Middleware.addBodyParser(); diff --git a/commitlint.config.ts b/commitlint.config.ts index 149bce3..3cbf29f 100644 --- a/commitlint.config.ts +++ b/commitlint.config.ts @@ -1,7 +1,12 @@ -import type { UserConfig } from '@commitlint/types' +import type { UserConfig } from "@commitlint/types"; const Configuration: UserConfig = { - extends: ['@commitlint/config-conventional'] -} + extends: ["@commitlint/config-conventional"], + rules: { + "subject-max-length": [0], + "body-max-line-length": [0], + "header-max-length": [0], + }, +}; -export default Configuration \ No newline at end of file +export default Configuration; From 944f07185f9d3648db146d5318330ae5f795e315 Mon Sep 17 00:00:00 2001 From: Richard Zampieri Date: Mon, 5 Jan 2026 03:08:20 -0800 Subject: [PATCH 10/21] refactor: update middleware method for improved clarity and functionality --- application/src/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/app.ts b/application/src/app.ts index 90f4c7d..80539f4 100644 --- a/application/src/app.ts +++ b/application/src/app.ts @@ -10,7 +10,7 @@ export class App extends AppExpress { globalConfiguration(): void {} async configureServices(): Promise { - this.Middleware.addBodyParser(); + this.Middleware.parse(); this.Middleware.setErrorHandler({ showStackTrace: await this.isDevelopment(), }); From dde7417c6e7dfb3b21526269b0c9fdfa822e5086 Mon Sep 17 00:00:00 2001 From: Richard Zampieri Date: Mon, 5 Jan 2026 03:11:14 -0800 Subject: [PATCH 11/21] refactor: replace middleware parsing with placeholder for future implementation --- application/src/app.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/application/src/app.ts b/application/src/app.ts index 80539f4..cb2d114 100644 --- a/application/src/app.ts +++ b/application/src/app.ts @@ -10,7 +10,8 @@ export class App extends AppExpress { globalConfiguration(): void {} async configureServices(): Promise { - this.Middleware.parse(); + // __MIDDLEWARE_PRESET_PLACEHOLDER__ + this.Middleware.setErrorHandler({ showStackTrace: await this.isDevelopment(), }); From 8a6414c2bc900b8c96a4bbe1fcbdd6fe08dd3cc1 Mon Sep 17 00:00:00 2001 From: Richard Zampieri Date: Mon, 5 Jan 2026 12:59:37 -0800 Subject: [PATCH 12/21] chore: temporary remove expressots packages for testing --- application/package.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/application/package.json b/application/package.json index 725e7b3..cbc9dc3 100644 --- a/application/package.json +++ b/application/package.json @@ -16,14 +16,10 @@ "lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\" --fix" }, "dependencies": { - "@expressots/adapter-express": "file:../adapter-express/expressots-adapter-express-4.0.0-beta.1.tgz", - "@expressots/core": "file:../expressots/expressots-core-4.0.0-beta.1.tgz", - "@expressots/shared": "file:../shared/expressots-shared-4.0.0-beta.1.tgz", "express": "5.2.1" }, "devDependencies": { "@eslint/js": "9.39.2", - "@expressots/cli": "file:../expressots-cli/expressots-cli-4.0.0-beta.1.tgz", "@types/express": "5.0.6", "@types/jest": "30.0.0", "@types/node": "25.0.3", From bc3324bedc59a44eb532733bb589c5cabb09fa40 Mon Sep 17 00:00:00 2001 From: Richard Zampieri Date: Wed, 7 Jan 2026 23:39:26 -0800 Subject: [PATCH 13/21] feat: add ExpressoTS CLI templates repository with CI/CD, Docker, Kubernetes, and migration templates - Introduced a new repository structure for community-maintained templates. - Added templates for various CI/CD platforms (GitHub Actions, GitLab, CircleCI, Jenkins, Bitbucket, Azure). - Included Docker configurations for production and development environments. - Provided Kubernetes manifests for deployments, services, config maps, and ingress. - Created migration checklists for transitioning from Heroku to Railway, Render, and Fly.io. - Implemented a manifest and pricing data structure for template versioning and cloud provider pricing. --- README.md | 280 ++++++++++++------ cli-templates/README.md | 70 +++++ cli-templates/cicd/azure/basic.yml | 43 +++ cli-templates/cicd/bitbucket/basic.yml | 72 +++++ cli-templates/cicd/circleci/basic.yml | 59 ++++ cli-templates/cicd/circleci/comprehensive.yml | 99 +++++++ cli-templates/cicd/github/basic.yml | 79 +++++ cli-templates/cicd/github/comprehensive.yml | 183 ++++++++++++ .../cicd/github/security-focused.yml | 209 +++++++++++++ cli-templates/cicd/gitlab/basic.yml | 50 ++++ cli-templates/cicd/gitlab/comprehensive.yml | 86 ++++++ .../cicd/gitlab/security-focused.yml | 97 ++++++ cli-templates/cicd/jenkins/basic.groovy | 51 ++++ .../docker/Dockerfile.development.tpl | 35 +++ .../docker/Dockerfile.production.tpl | 59 ++++ .../docker/docker-compose.development.yml.tpl | 28 ++ cli-templates/docker/docker-compose.yml.tpl | 30 ++ cli-templates/kubernetes/configmap.yml.tpl | 17 ++ cli-templates/kubernetes/deployment.yml.tpl | 54 ++++ cli-templates/kubernetes/ingress.yml.tpl | 30 ++ cli-templates/kubernetes/service.yml.tpl | 20 ++ cli-templates/manifest.json | 147 +++++++++ .../compose-to-kubernetes/checklist.md.tpl | 48 +++ .../migrations/heroku-to-fly/checklist.md.tpl | 41 +++ .../heroku-to-railway/checklist.md.tpl | 54 ++++ .../heroku-to-render/checklist.md.tpl | 42 +++ cli-templates/pricing.json | 121 ++++++++ 27 files changed, 2020 insertions(+), 84 deletions(-) create mode 100644 cli-templates/README.md create mode 100644 cli-templates/cicd/azure/basic.yml create mode 100644 cli-templates/cicd/bitbucket/basic.yml create mode 100644 cli-templates/cicd/circleci/basic.yml create mode 100644 cli-templates/cicd/circleci/comprehensive.yml create mode 100644 cli-templates/cicd/github/basic.yml create mode 100644 cli-templates/cicd/github/comprehensive.yml create mode 100644 cli-templates/cicd/github/security-focused.yml create mode 100644 cli-templates/cicd/gitlab/basic.yml create mode 100644 cli-templates/cicd/gitlab/comprehensive.yml create mode 100644 cli-templates/cicd/gitlab/security-focused.yml create mode 100644 cli-templates/cicd/jenkins/basic.groovy create mode 100644 cli-templates/docker/Dockerfile.development.tpl create mode 100644 cli-templates/docker/Dockerfile.production.tpl create mode 100644 cli-templates/docker/docker-compose.development.yml.tpl create mode 100644 cli-templates/docker/docker-compose.yml.tpl create mode 100644 cli-templates/kubernetes/configmap.yml.tpl create mode 100644 cli-templates/kubernetes/deployment.yml.tpl create mode 100644 cli-templates/kubernetes/ingress.yml.tpl create mode 100644 cli-templates/kubernetes/service.yml.tpl create mode 100644 cli-templates/manifest.json create mode 100644 cli-templates/migrations/compose-to-kubernetes/checklist.md.tpl create mode 100644 cli-templates/migrations/heroku-to-fly/checklist.md.tpl create mode 100644 cli-templates/migrations/heroku-to-railway/checklist.md.tpl create mode 100644 cli-templates/migrations/heroku-to-render/checklist.md.tpl create mode 100644 cli-templates/pricing.json diff --git a/README.md b/README.md index 86626a6..e6dbd80 100644 --- a/README.md +++ b/README.md @@ -1,99 +1,211 @@ - - - -[![Version][version-shield]][version-url] -[![MIT License][license-shield]][license-url] -[![LinkedIn][linkedin-shield]][linkedin-url] - - -
-
- - Logo - - -

ExpressoTS Framework

- -

- Everything you need to know to build applications with ExpressoTS -
- Explore the docs ยป -
-
- Let's discuss - ยท - Report Bug - ยท - Request Feature -

-
- - -
- Table of Contents -
    -
  1. About The Project
  2. -
  3. Getting Started
  4. -
  5. Contributing
  6. -
  7. Support the project
  8. -
  9. License
  10. -
-
- - -# About The Project - -ExpressoTS is a [Typescript](https://www.typescriptlang.org/) + [Node.js](https://nodejs.org/en/) lightweight framework for quick building scalable, easy to read and maintain, server-side applications ๐ŸŽ - -## Getting Started - -- Here is our [Site](https://expresso-ts.com/) -- You can find our [Documentation here](https://doc.expresso-ts.com/) -- Checkout our [First Steps documentation](https://doc.expresso-ts.com/docs/overview/first-steps) -- Our [CLI Documentation](https://doc.expresso-ts.com/docs/cli/overview) +# ExpressoTS Templates Repository + +Community-maintained templates and pricing data for the [ExpressoTS CLI](https://github.com/expressots/expressots-cli). + +## What's This? -## Contributing +This repository contains all the templates used by ExpressoTS CLI to generate: +- **CI/CD Pipelines** (GitHub Actions, GitLab CI, CircleCI, Jenkins, etc.) +- **Docker Configurations** (Dockerfiles, docker-compose) +- **Kubernetes Manifests** (deployments, services, ingress) +- **Migration Guides** (Heroku โ†’ Railway/Render/Fly, Compose โ†’ K8s) +- **Cloud Provider Pricing** (for cost estimation) + +## Why a Separate Repository? + +**Fast Updates:** Templates can be updated without releasing a new CLI version. Users get updates within 24 hours. + +**Community Contributions:** Anyone can contribute templates or update pricing - no CLI expertise needed. + +**Offline Support:** CLI has embedded fallbacks, so it works without internet. + +**Version Control:** Templates are versioned independently from the CLI. + +## Repository Structure + +``` +cli-templates/ +โ”œโ”€โ”€ manifest.json # Template registry with versions +โ”œโ”€โ”€ pricing.json # Cloud provider pricing data +โ”œโ”€โ”€ README.md # This file +โ”œโ”€โ”€ cicd/ # CI/CD pipeline templates +โ”‚ โ”œโ”€โ”€ github/ +โ”‚ โ”‚ โ”œโ”€โ”€ basic.yml +โ”‚ โ”‚ โ”œโ”€โ”€ comprehensive.yml +โ”‚ โ”‚ โ””โ”€โ”€ security-focused.yml +โ”‚ โ”œโ”€โ”€ gitlab/ +โ”‚ โ”œโ”€โ”€ circleci/ +โ”‚ โ”œโ”€โ”€ jenkins/ +โ”‚ โ”œโ”€โ”€ bitbucket/ +โ”‚ โ””โ”€โ”€ azure/ +โ”œโ”€โ”€ docker/ # Docker templates +โ”‚ โ”œโ”€โ”€ Dockerfile.production.tpl +โ”‚ โ”œโ”€โ”€ Dockerfile.development.tpl +โ”‚ โ”œโ”€โ”€ docker-compose.yml.tpl +โ”‚ โ””โ”€โ”€ docker-compose.development.yml.tpl +โ”œโ”€โ”€ kubernetes/ # Kubernetes manifest templates +โ”‚ โ”œโ”€โ”€ deployment.yml.tpl +โ”‚ โ”œโ”€โ”€ service.yml.tpl +โ”‚ โ”œโ”€โ”€ configmap.yml.tpl +โ”‚ โ””โ”€โ”€ ingress.yml.tpl +โ””โ”€โ”€ migrations/ # Migration templates + โ”œโ”€โ”€ heroku-to-railway/ + โ”œโ”€โ”€ heroku-to-render/ + โ”œโ”€โ”€ heroku-to-fly/ + โ””โ”€โ”€ compose-to-kubernetes/ +``` + +## How to Use + +### For Users -Welcome to the ExpressoTS community, a place bustling with innovative minds just like yours. We're absolutely thrilled to have you here! -ExpressoTS is more than just a TypeScript framework; it's a collective effort by developers who are passionate about creating a more efficient, secure, and robust web ecosystem. We firmly believe that the best ideas come from a diversity of perspectives, backgrounds, and skills. +Users don't need to interact with this repository directly. The ExpressoTS CLI automatically fetches templates: -Why Contribute to Documentation? +```bash +# List available templates +expressots templates list -- **Share Knowledge**: If you've figured out something cool, why keep it to yourself? -- **Build Your Portfolio**: Contributing to an open-source project like ExpressoTS is a great way to showcase your skills. -- **Join a Network**: Get to know a community of like-minded developers. -- **Improve the Product**: Help us fill in the gaps, correct errors, or make complex topics easier to understand. +# Update to latest versions +expressots templates update -Ready to contribute? +# Generate CI/CD pipeline +expressots cicd generate github --strategy comprehensive +``` -- [Contributing Guidelines](https://github.com/expressots/expressots/blob/main/CONTRIBUTING.md) -- [How to Contribute](https://github.com/expressots/expressots/blob/main/CONTRIBUTING_HOWTO.md) -- [Coding Guidelines](https://github.com/rsaz/TypescriptCodingGuidelines) +### For Contributors -## Support the project +Want to improve templates or update pricing? See [CONTRIBUTING.md](./CONTRIBUTING.md) for detailed instructions. -ExpressoTS is an independent open source project with ongoing development made possible thanks to your support. If you'd like to help, please consider: +**Quick start:** -- Become a **[sponsor on GitHub](https://github.com/sponsors/expressots)** -- Follow the **[organization](https://github.com/expressots)** on GitHub and Star โญ the project -- Subscribe to the Twitch channel: **[Richard Zampieri](https://www.twitch.tv/richardzampieri)** -- Join our **[Discord](https://discord.com/invite/PyPJfGK)** -- Contribute submitting **[issues and pull requests](https://github.com/expressots/expressots/issues)** -- Share the project with your friends and colleagues +1. Fork this repository +2. Edit a template file (e.g., `cicd/github/basic.yml`) +3. Update `manifest.json` with new version +4. Test locally: + ```bash + expressots templates repo set YOUR_USERNAME/templates + expressots templates update + expressots cicd generate github + ``` +5. Submit a pull request + +### For Organizations + +Use custom templates for your organization: + +```bash +# Fork this repo, customize, then: +expressots templates repo set mycompany/expressots-templates + +# Team members automatically get company templates +expressots cicd generate github +``` + +## Template Syntax + +Templates use Mustache-like syntax: + +```yaml +name: {{projectName}} + +{{#includeSecurity}} +security: + scan: enabled +{{/includeSecurity}} + +{{^isProduction}} +dev-tools: + - nodemon +{{/isProduction}} +``` + +**Supported:** +- Variables: `{{name}}` +- Conditionals: `{{#cond}}...{{/cond}}` +- Negative conditionals: `{{^cond}}...{{/cond}}` +- Loops: `{{#each items}}...{{/each}}` + +## Contributing + +We welcome contributions! See [CONTRIBUTING.md](./CONTRIBUTING.md) for: +- How to add new templates +- How to update pricing data +- Testing guidelines +- Review process +- Best practices + +**Common contributions:** +- Add new CI/CD strategies +- Update cloud provider pricing +- Improve security scanning +- Add new platform support +- Fix bugs in templates + +## Pricing Data + +Cloud provider pricing is stored in `cli-templates/pricing.json` and used by the CLI's cost estimation features. + +**Updating pricing:** + +1. Visit official pricing pages (links in `pricing.json`) +2. Update prices for a provider +3. Update `lastVerified` date +4. Submit PR with source URLs + +**Example:** +```json +{ + "providers": { + "aws": { + "cpuPerHour": 0.04048, + "source": "https://aws.amazon.com/fargate/pricing/", + "lastVerified": "2026-01-15" + } + } +} +``` + +## Versioning + +### Template Versions + +Each template has its own version: +- **Patch (1.0.1):** Bug fixes, typos +- **Minor (1.1.0):** New features, backward compatible +- **Major (2.0.0):** Breaking changes + +### Manifest Version + +The manifest version tracks overall structure changes: +```json +{ + "version": "1.0.0", + "updated": "2026-01-07T00:00:00Z" +} +``` + +## Release Process + +1. PRs merged to `main` branch +2. CI validates manifest and templates +3. Changes immediately available to CLI users +4. Users' local cache updates within 24 hours +5. Or manually: `expressots templates update` + +## Support + +- **Documentation:** https://expresso-ts.com/docs/cli/templates +- **Issues:** https://github.com/expressots/expressots-cli/issues +- **Discord:** https://discord.gg/expressots ## License -Distributed under the MIT License. See [`LICENSE.txt`](https://github.com/expressots/expressots/blob/main/LICENSE) for more information. +MIT License - see [LICENSE](./LICENSE) for details. + +## Acknowledgments -

(back to top)

+This repository is maintained by the ExpressoTS community. Special thanks to all contributors who help keep templates and pricing data up-to-date! - - +--- -[version-shield]: https://img.shields.io/github/v/tag/expressots/templates?style=for-the-badge&logo=github -[version-url]: https://img.shields.io/github/v/tag/expressots/templates?style=for-the-badge&logo=github -[license-shield]: https://img.shields.io/github/license/expressots/expressots-project-template?style=for-the-badge -[license-url]: https://github.com/expressots/expressots-project-template/blob/main/LICENSE -[linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=for-the-badge&logo=linkedin&colorB=555 -[linkedin-url]: https://www.linkedin.com/company/expresso-ts/ -[product-screenshot]: images/screenshot.png +**Built with โค๏ธ by the ExpressoTS community** diff --git a/cli-templates/README.md b/cli-templates/README.md new file mode 100644 index 0000000..d360cc9 --- /dev/null +++ b/cli-templates/README.md @@ -0,0 +1,70 @@ +# ExpressoTS CLI Templates + +This directory contains templates used by the ExpressoTS CLI for generating CI/CD pipelines, Docker configurations, Kubernetes manifests, and migration guides. + +## Structure + +``` +cli-templates/ +โ”œโ”€โ”€ manifest.json # Template registry with versions +โ”œโ”€โ”€ cicd/ # CI/CD pipeline templates +โ”‚ โ”œโ”€โ”€ github/ +โ”‚ โ”‚ โ”œโ”€โ”€ basic.yml +โ”‚ โ”‚ โ”œโ”€โ”€ comprehensive.yml +โ”‚ โ”‚ โ””โ”€โ”€ security-focused.yml +โ”‚ โ”œโ”€โ”€ gitlab/ +โ”‚ โ”‚ โ”œโ”€โ”€ basic.yml +โ”‚ โ”‚ โ”œโ”€โ”€ comprehensive.yml +โ”‚ โ”‚ โ””โ”€โ”€ security-focused.yml +โ”‚ โ”œโ”€โ”€ circleci/ +โ”‚ โ”œโ”€โ”€ jenkins/ +โ”‚ โ”œโ”€โ”€ bitbucket/ +โ”‚ โ””โ”€โ”€ azure/ +โ”œโ”€โ”€ docker/ # Docker templates +โ”‚ โ”œโ”€โ”€ Dockerfile.production.tpl +โ”‚ โ”œโ”€โ”€ Dockerfile.development.tpl +โ”‚ โ”œโ”€โ”€ docker-compose.yml.tpl +โ”‚ โ””โ”€โ”€ docker-compose.development.yml.tpl +โ”œโ”€โ”€ kubernetes/ # Kubernetes manifest templates +โ”‚ โ”œโ”€โ”€ deployment.yml.tpl +โ”‚ โ”œโ”€โ”€ service.yml.tpl +โ”‚ โ”œโ”€โ”€ configmap.yml.tpl +โ”‚ โ””โ”€โ”€ ingress.yml.tpl +โ””โ”€โ”€ migrations/ # Migration templates + โ”œโ”€โ”€ heroku-to-railway/ + โ”œโ”€โ”€ heroku-to-render/ + โ”œโ”€โ”€ heroku-to-fly/ + โ””โ”€โ”€ compose-to-kubernetes/ +``` + +## Template Syntax + +Templates use Mustache-like syntax: + +- `{{variable}}` - Variable substitution +- `{{#condition}}...{{/condition}}` - Conditional blocks +- `{{^condition}}...{{/condition}}` - Negative conditionals + +## Usage + +```bash +# List available templates +expressots templates list + +# Update cache +expressots templates update + +# Show status +expressots templates status +``` + +## Contributing + +1. Fork this repository +2. Add or modify templates +3. Update manifest.json +4. Submit a pull request + +## License + +MIT License diff --git a/cli-templates/cicd/azure/basic.yml b/cli-templates/cicd/azure/basic.yml new file mode 100644 index 0000000..df2acd8 --- /dev/null +++ b/cli-templates/cicd/azure/basic.yml @@ -0,0 +1,43 @@ +# Azure DevOps Pipeline +# Generated by ExpressoTS CLI +# Strategy: basic +# Template Version: 1.0.0 + +trigger: + - {{branch}} + +pool: + vmImage: 'ubuntu-latest' + +variables: + nodeVersion: '{{nodeVersion}}' + +stages: + - stage: Build + displayName: 'Build Stage' + jobs: + - job: BuildJob + displayName: 'Build, Lint, and Test' + steps: + - task: NodeTool@0 + inputs: + versionSpec: '$(nodeVersion)' + displayName: 'Install Node.js' + + - script: {{installCmd}} + displayName: 'Install dependencies' + + - script: {{lintCmd}} + displayName: 'Run linter' + + - script: {{testCmd}} + displayName: 'Run tests' + + - script: {{buildCmd}} + displayName: 'Build application' + + - task: PublishBuildArtifacts@1 + inputs: + pathToPublish: 'dist' + artifactName: 'dist' + displayName: 'Publish build artifacts' diff --git a/cli-templates/cicd/bitbucket/basic.yml b/cli-templates/cicd/bitbucket/basic.yml new file mode 100644 index 0000000..290f932 --- /dev/null +++ b/cli-templates/cicd/bitbucket/basic.yml @@ -0,0 +1,72 @@ +# Bitbucket Pipelines +# Generated by ExpressoTS CLI +# Strategy: basic +# Template Version: 1.0.0 + +image: node:{{nodeVersion}}-alpine + +definitions: + caches: + npm: $HOME/.npm + +pipelines: + default: + - step: + name: Lint + caches: + - npm + - node + script: + - {{installCmd}} + - {{lintCmd}} + + - step: + name: Test + caches: + - npm + - node + script: + - {{installCmd}} + - {{testCmd}} + + - step: + name: Build + caches: + - npm + - node + script: + - {{installCmd}} + - {{buildCmd}} + artifacts: + - dist/** + + branches: + {{branch}}: + - step: + name: Lint + caches: + - npm + - node + script: + - {{installCmd}} + - {{lintCmd}} + + - step: + name: Test + caches: + - npm + - node + script: + - {{installCmd}} + - {{testCmd}} + + - step: + name: Build + caches: + - npm + - node + script: + - {{installCmd}} + - {{buildCmd}} + artifacts: + - dist/** diff --git a/cli-templates/cicd/circleci/basic.yml b/cli-templates/cicd/circleci/basic.yml new file mode 100644 index 0000000..bcaa6e2 --- /dev/null +++ b/cli-templates/cicd/circleci/basic.yml @@ -0,0 +1,59 @@ +# CircleCI Configuration +# Generated by ExpressoTS CLI +# Strategy: basic +# Template Version: 1.0.0 + +version: 2.1 + +orbs: + node: circleci/node@5.1.0 + +jobs: + lint: + executor: + name: node/default + tag: '{{nodeVersion}}' + steps: + - checkout + - node/install-packages + - run: + name: Run linter + command: {{lintCmd}} + + test: + executor: + name: node/default + tag: '{{nodeVersion}}' + steps: + - checkout + - node/install-packages + - run: + name: Run tests + command: {{testCmd}} + + build: + executor: + name: node/default + tag: '{{nodeVersion}}' + steps: + - checkout + - node/install-packages + - run: + name: Build application + command: {{buildCmd}} + - persist_to_workspace: + root: . + paths: + - dist + +workflows: + version: 2 + build-and-test: + jobs: + - lint + - test: + requires: + - lint + - build: + requires: + - test diff --git a/cli-templates/cicd/circleci/comprehensive.yml b/cli-templates/cicd/circleci/comprehensive.yml new file mode 100644 index 0000000..db07cd0 --- /dev/null +++ b/cli-templates/cicd/circleci/comprehensive.yml @@ -0,0 +1,99 @@ +# CircleCI Configuration +# Generated by ExpressoTS CLI +# Strategy: comprehensive +# Template Version: 1.0.0 + +version: 2.1 + +orbs: + node: circleci/node@5.1.0 + docker: circleci/docker@2.4.0 + +jobs: + lint: + executor: + name: node/default + tag: '{{nodeVersion}}' + steps: + - checkout + - node/install-packages + - run: + name: Run linter + command: {{lintCmd}} + + test: + executor: + name: node/default + tag: '{{nodeVersion}}' + steps: + - checkout + - node/install-packages + - run: + name: Run tests + command: {{testCmd}} +{{#includeCoverage}} + - run: + name: Run tests with coverage + command: {{testCmd}} -- --coverage + - store_artifacts: + path: coverage +{{/includeCoverage}} + + security: + docker: + - image: aquasec/trivy:latest + steps: + - checkout + - run: + name: Run security scan + command: trivy fs --severity HIGH,CRITICAL . + + build: + executor: + name: node/default + tag: '{{nodeVersion}}' + steps: + - checkout + - node/install-packages + - run: + name: Build application + command: {{buildCmd}} + - persist_to_workspace: + root: . + paths: + - dist + + docker-build: + executor: docker/docker + steps: + - checkout + - setup_remote_docker + - docker/check + - docker/build: + image: {{dockerRegistry}}/$CIRCLE_PROJECT_REPONAME + tag: $CIRCLE_SHA1 + - docker/push: + image: {{dockerRegistry}}/$CIRCLE_PROJECT_REPONAME + tag: $CIRCLE_SHA1 + +workflows: + version: 2 + build-test-deploy: + jobs: + - lint + - test: + requires: + - lint + - security: + requires: + - lint + - build: + requires: + - test + - security + - docker-build: + requires: + - build + filters: + branches: + only: {{branch}} diff --git a/cli-templates/cicd/github/basic.yml b/cli-templates/cicd/github/basic.yml new file mode 100644 index 0000000..c83ded0 --- /dev/null +++ b/cli-templates/cicd/github/basic.yml @@ -0,0 +1,79 @@ +# GitHub Actions CI/CD Pipeline +# Generated by ExpressoTS CLI +# Strategy: {{strategy}} +# Template Version: 1.0.0 + +name: CI/CD Pipeline + +on: + push: + branches: [{{branch}}] + pull_request: + branches: [{{branch}}] + +env: + NODE_VERSION: '{{nodeVersion}}' + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: '{{packageManager}}' + + - name: Install dependencies + run: {{installCmd}} + + - name: Run linter + run: {{lintCmd}} + + test: + name: Test + runs-on: ubuntu-latest + needs: lint + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: '{{packageManager}}' + + - name: Install dependencies + run: {{installCmd}} + + - name: Run tests + run: {{testCmd}} + + build: + name: Build + runs-on: ubuntu-latest + needs: test + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: '{{packageManager}}' + + - name: Install dependencies + run: {{installCmd}} + + - name: Build application + run: {{buildCmd}} + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + retention-days: 7 diff --git a/cli-templates/cicd/github/comprehensive.yml b/cli-templates/cicd/github/comprehensive.yml new file mode 100644 index 0000000..80bef76 --- /dev/null +++ b/cli-templates/cicd/github/comprehensive.yml @@ -0,0 +1,183 @@ +# GitHub Actions CI/CD Pipeline +# Generated by ExpressoTS CLI +# Strategy: comprehensive +# Template Version: 1.0.0 + +name: CI/CD Pipeline + +on: + push: + branches: [{{branch}}] + pull_request: + branches: [{{branch}}] + +env: + NODE_VERSION: '{{nodeVersion}}' + REGISTRY: {{dockerRegistry}} + IMAGE_NAME: ${{ github.repository }} + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: '{{packageManager}}' + + - name: Install dependencies + run: {{installCmd}} + + - name: Run linter + run: {{lintCmd}} + + test: + name: Test + runs-on: ubuntu-latest + needs: lint + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: '{{packageManager}}' + + - name: Install dependencies + run: {{installCmd}} + + - name: Run tests + run: {{testCmd}} +{{#includeCoverage}} + + - name: Run tests with coverage + run: {{testCmd}} -- --coverage + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: false +{{/includeCoverage}} + + security: + name: Security Scan + runs-on: ubuntu-latest + needs: lint + steps: + - uses: actions/checkout@v4 + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + scan-type: 'fs' + scan-ref: '.' + format: 'sarif' + output: 'trivy-results.sarif' + + - name: Upload Trivy scan results + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: 'trivy-results.sarif' + +{{#includeE2E}} + e2e: + name: E2E Tests + runs-on: ubuntu-latest + needs: test + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: '{{packageManager}}' + + - name: Install dependencies + run: {{installCmd}} + + - name: Build application + run: {{buildCmd}} + + - name: Start application + run: | + npm start & + sleep 10 + + - name: Run E2E tests + run: npm run test:e2e +{{/includeE2E}} + + build: + name: Build + runs-on: ubuntu-latest + needs: [test, security{{#includeE2E}}, e2e{{/includeE2E}}] + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: '{{packageManager}}' + + - name: Install dependencies + run: {{installCmd}} + + - name: Build application + run: {{buildCmd}} + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + retention-days: 7 + + docker: + name: Docker Build + runs-on: ubuntu-latest + needs: build + if: github.event_name == 'push' && github.ref == 'refs/heads/{{branch}}' + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=sha,prefix= + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/cli-templates/cicd/github/security-focused.yml b/cli-templates/cicd/github/security-focused.yml new file mode 100644 index 0000000..d8791e8 --- /dev/null +++ b/cli-templates/cicd/github/security-focused.yml @@ -0,0 +1,209 @@ +# GitHub Actions CI/CD Pipeline +# Generated by ExpressoTS CLI +# Strategy: security-focused +# Template Version: 1.0.0 + +name: Security-Focused CI/CD Pipeline + +on: + push: + branches: [{{branch}}] + pull_request: + branches: [{{branch}}] + schedule: + - cron: '0 0 * * 0' # Weekly security scan + +env: + NODE_VERSION: '{{nodeVersion}}' + REGISTRY: {{dockerRegistry}} + IMAGE_NAME: ${{ github.repository }} + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: '{{packageManager}}' + + - name: Install dependencies + run: {{installCmd}} + + - name: Run linter + run: {{lintCmd}} + + test: + name: Test + runs-on: ubuntu-latest + needs: lint + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: '{{packageManager}}' + + - name: Install dependencies + run: {{installCmd}} + + - name: Run tests + run: {{testCmd}} + + security-trivy: + name: Trivy Security Scan + runs-on: ubuntu-latest + needs: lint + steps: + - uses: actions/checkout@v4 + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + scan-type: 'fs' + scan-ref: '.' + format: 'sarif' + output: 'trivy-results.sarif' + severity: 'CRITICAL,HIGH' + + - name: Upload Trivy scan results + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: 'trivy-results.sarif' + + security-snyk: + name: Snyk Security Scan + runs-on: ubuntu-latest + needs: lint + steps: + - uses: actions/checkout@v4 + + - name: Run Snyk security scan + uses: snyk/actions/node@master + continue-on-error: true + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + with: + args: --severity-threshold=high + + security-npm-audit: + name: NPM Audit + runs-on: ubuntu-latest + needs: lint + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Run npm audit + run: npm audit --audit-level=high + + security-owasp: + name: OWASP Dependency Check + runs-on: ubuntu-latest + needs: lint + steps: + - uses: actions/checkout@v4 + + - name: OWASP Dependency Check + uses: dependency-check/Dependency-Check_Action@main + with: + project: '{{projectName}}' + path: '.' + format: 'HTML' + + - name: Upload OWASP report + uses: actions/upload-artifact@v4 + with: + name: owasp-report + path: reports/ + + build: + name: Build + runs-on: ubuntu-latest + needs: [test, security-trivy, security-snyk, security-npm-audit] + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: '{{packageManager}}' + + - name: Install dependencies + run: {{installCmd}} + + - name: Build application + run: {{buildCmd}} + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + retention-days: 7 + + docker: + name: Docker Build & Scan + runs-on: ubuntu-latest + needs: build + if: github.event_name == 'push' && github.ref == 'refs/heads/{{branch}}' + permissions: + contents: read + packages: write + security-events: write + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build image + uses: docker/build-push-action@v5 + with: + context: . + load: true + tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} + + - name: Scan container for vulnerabilities + uses: aquasecurity/trivy-action@master + with: + image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} + format: 'sarif' + output: 'container-scan.sarif' + severity: 'CRITICAL,HIGH' + + - name: Upload container scan results + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: 'container-scan.sarif' + + - name: Push image + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: | + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/cli-templates/cicd/gitlab/basic.yml b/cli-templates/cicd/gitlab/basic.yml new file mode 100644 index 0000000..e6c5afd --- /dev/null +++ b/cli-templates/cicd/gitlab/basic.yml @@ -0,0 +1,50 @@ +# GitLab CI/CD Pipeline +# Generated by ExpressoTS CLI +# Strategy: basic +# Template Version: 1.0.0 + +image: node:{{nodeVersion}}-alpine + +stages: + - lint + - test + - build + +variables: + npm_config_cache: "$CI_PROJECT_DIR/.npm" + +cache: + key: ${CI_COMMIT_REF_SLUG} + paths: + - .npm/ + - node_modules/ + +lint: + stage: lint + script: + - {{installCmd}} + - {{lintCmd}} + only: + - {{branch}} + - merge_requests + +test: + stage: test + script: + - {{installCmd}} + - {{testCmd}} + only: + - {{branch}} + - merge_requests + +build: + stage: build + script: + - {{installCmd}} + - {{buildCmd}} + artifacts: + paths: + - dist/ + expire_in: 1 week + only: + - {{branch}} diff --git a/cli-templates/cicd/gitlab/comprehensive.yml b/cli-templates/cicd/gitlab/comprehensive.yml new file mode 100644 index 0000000..63ccf8c --- /dev/null +++ b/cli-templates/cicd/gitlab/comprehensive.yml @@ -0,0 +1,86 @@ +# GitLab CI/CD Pipeline +# Generated by ExpressoTS CLI +# Strategy: comprehensive +# Template Version: 1.0.0 + +image: node:{{nodeVersion}}-alpine + +stages: + - lint + - test + - security + - build + - docker + - deploy + +variables: + npm_config_cache: "$CI_PROJECT_DIR/.npm" + DOCKER_IMAGE: $CI_REGISTRY_IMAGE + +cache: + key: ${CI_COMMIT_REF_SLUG} + paths: + - .npm/ + - node_modules/ + +lint: + stage: lint + script: + - {{installCmd}} + - {{lintCmd}} + only: + - {{branch}} + - merge_requests + +test: + stage: test + script: + - {{installCmd}} + - {{testCmd}}{{#includeCoverage}} --coverage{{/includeCoverage}} +{{#includeCoverage}} + coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)/' + artifacts: + reports: + coverage_report: + coverage_format: cobertura + path: coverage/cobertura-coverage.xml +{{/includeCoverage}} + only: + - {{branch}} + - merge_requests + +security: + stage: security + image: aquasec/trivy:latest + script: + - trivy fs --severity HIGH,CRITICAL --exit-code 1 . + allow_failure: true + only: + - {{branch}} + - merge_requests + +build: + stage: build + script: + - {{installCmd}} + - {{buildCmd}} + artifacts: + paths: + - dist/ + expire_in: 1 week + only: + - {{branch}} + +docker: + stage: docker + image: docker:latest + services: + - docker:dind + before_script: + - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY + script: + - docker build -t $DOCKER_IMAGE:$CI_COMMIT_SHA -t $DOCKER_IMAGE:latest . + - docker push $DOCKER_IMAGE:$CI_COMMIT_SHA + - docker push $DOCKER_IMAGE:latest + only: + - {{branch}} diff --git a/cli-templates/cicd/gitlab/security-focused.yml b/cli-templates/cicd/gitlab/security-focused.yml new file mode 100644 index 0000000..90f8946 --- /dev/null +++ b/cli-templates/cicd/gitlab/security-focused.yml @@ -0,0 +1,97 @@ +# GitLab CI/CD Pipeline +# Generated by ExpressoTS CLI +# Strategy: security-focused +# Template Version: 1.0.0 + +image: node:{{nodeVersion}}-alpine + +stages: + - lint + - test + - security + - sast + - build + - container-scanning + - deploy + +include: + - template: Security/SAST.gitlab-ci.yml + - template: Security/Dependency-Scanning.gitlab-ci.yml + - template: Security/Secret-Detection.gitlab-ci.yml + - template: Security/Container-Scanning.gitlab-ci.yml + +variables: + npm_config_cache: "$CI_PROJECT_DIR/.npm" + DOCKER_IMAGE: $CI_REGISTRY_IMAGE + SAST_EXCLUDED_PATHS: "node_modules, dist, coverage" + +cache: + key: ${CI_COMMIT_REF_SLUG} + paths: + - .npm/ + - node_modules/ + +lint: + stage: lint + script: + - {{installCmd}} + - {{lintCmd}} + only: + - {{branch}} + - merge_requests + +test: + stage: test + script: + - {{installCmd}} + - {{testCmd}} + only: + - {{branch}} + - merge_requests + +npm-audit: + stage: security + script: + - npm audit --audit-level=high + allow_failure: true + only: + - {{branch}} + - merge_requests + +trivy-scan: + stage: security + image: aquasec/trivy:latest + script: + - trivy fs --severity CRITICAL,HIGH --exit-code 1 --format sarif -o trivy-results.sarif . + artifacts: + reports: + sast: trivy-results.sarif + allow_failure: false + only: + - {{branch}} + - merge_requests + +build: + stage: build + script: + - {{installCmd}} + - {{buildCmd}} + artifacts: + paths: + - dist/ + expire_in: 1 week + only: + - {{branch}} + +docker: + stage: container-scanning + image: docker:latest + services: + - docker:dind + before_script: + - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY + script: + - docker build -t $DOCKER_IMAGE:$CI_COMMIT_SHA . + - docker push $DOCKER_IMAGE:$CI_COMMIT_SHA + only: + - {{branch}} diff --git a/cli-templates/cicd/jenkins/basic.groovy b/cli-templates/cicd/jenkins/basic.groovy new file mode 100644 index 0000000..357d9c8 --- /dev/null +++ b/cli-templates/cicd/jenkins/basic.groovy @@ -0,0 +1,51 @@ +// Jenkinsfile +// Generated by ExpressoTS CLI +// Strategy: basic +// Template Version: 1.0.0 + +pipeline { + agent { + docker { + image 'node:{{nodeVersion}}-alpine' + } + } + + environment { + npm_config_cache = "${WORKSPACE}/.npm" + } + + stages { + stage('Install') { + steps { + sh '{{installCmd}}' + } + } + + stage('Lint') { + steps { + sh '{{lintCmd}}' + } + } + + stage('Test') { + steps { + sh '{{testCmd}}' + } + } + + stage('Build') { + steps { + sh '{{buildCmd}}' + } + } + } + + post { + always { + cleanWs() + } + success { + archiveArtifacts artifacts: 'dist/**/*', fingerprint: true + } + } +} diff --git a/cli-templates/docker/Dockerfile.development.tpl b/cli-templates/docker/Dockerfile.development.tpl new file mode 100644 index 0000000..614b04e --- /dev/null +++ b/cli-templates/docker/Dockerfile.development.tpl @@ -0,0 +1,35 @@ +# Development Dockerfile +# Generated by ExpressoTS CLI +# Template Version: 1.0.0 + +FROM node:{{nodeVersion}}-alpine + +WORKDIR /app + +# Install development tools +RUN apk add --no-cache git + +# Copy package files +{{#hasLocalDeps}} +COPY package.docker.json ./package.json +COPY .docker-deps/ ./.docker-deps/ +{{/hasLocalDeps}} +{{^hasLocalDeps}} +COPY package*.json ./ +{{/hasLocalDeps}} + +# Install all dependencies (including dev) +RUN {{installCommand}} + +# Copy source code +COPY . . + +# Set environment variables +ENV NODE_ENV=development +ENV PORT={{port}} + +# Expose port +EXPOSE {{port}} + +# Start with hot reload +CMD ["npm", "run", "dev"] diff --git a/cli-templates/docker/Dockerfile.production.tpl b/cli-templates/docker/Dockerfile.production.tpl new file mode 100644 index 0000000..c58eb75 --- /dev/null +++ b/cli-templates/docker/Dockerfile.production.tpl @@ -0,0 +1,59 @@ +# Production Dockerfile +# Generated by ExpressoTS CLI +# Template Version: 1.0.0 + +# Build stage +FROM node:{{nodeVersion}}-alpine AS builder + +WORKDIR /app + +# Copy package files +{{#hasLocalDeps}} +COPY package.docker.json ./package.json +{{/hasLocalDeps}} +{{^hasLocalDeps}} +COPY package*.json ./ +{{/hasLocalDeps}} + +# Install dependencies +RUN {{installCommand}} + +# Copy source code +COPY . . + +# Build the application +RUN {{buildCommand}} + +# Prune dev dependencies +RUN npm prune --production + +# Production stage +FROM node:{{nodeVersion}}-alpine AS production + +WORKDIR /app + +# Create non-root user +RUN addgroup -g 1001 -S nodejs && \ + adduser -S expressots -u 1001 -G nodejs + +# Copy built files from builder +COPY --from=builder --chown=expressots:nodejs /app/node_modules ./node_modules +COPY --from=builder --chown=expressots:nodejs /app/dist ./dist +COPY --from=builder --chown=expressots:nodejs /app/package.json ./ + +# Set environment variables +ENV NODE_ENV=production +ENV PORT={{port}} + +# Switch to non-root user +USER expressots + +# Expose port +EXPOSE {{port}} + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:{{port}}{{healthCheckEndpoint}} || exit 1 + +# Start the application +CMD ["node", "{{entryPoint}}"] diff --git a/cli-templates/docker/docker-compose.development.yml.tpl b/cli-templates/docker/docker-compose.development.yml.tpl new file mode 100644 index 0000000..d71a398 --- /dev/null +++ b/cli-templates/docker/docker-compose.development.yml.tpl @@ -0,0 +1,28 @@ +# Docker Compose - Development +# Generated by ExpressoTS CLI +# Template Version: 1.0.0 + +version: '3.8' + +services: + app: + build: + context: . + dockerfile: Dockerfile.development + container_name: {{projectName}}-dev + ports: + - "{{port}}:{{port}}" + environment: + - NODE_ENV=development + - PORT={{port}} + volumes: + - ./src:/app/src:ro + - ./package.json:/app/package.json:ro + - ./tsconfig.json:/app/tsconfig.json:ro + - ./expressots.config.ts:/app/expressots.config.ts:ro + networks: + - dev-network + +networks: + dev-network: + driver: bridge diff --git a/cli-templates/docker/docker-compose.yml.tpl b/cli-templates/docker/docker-compose.yml.tpl new file mode 100644 index 0000000..579585b --- /dev/null +++ b/cli-templates/docker/docker-compose.yml.tpl @@ -0,0 +1,30 @@ +# Docker Compose - Production +# Generated by ExpressoTS CLI +# Template Version: 1.0.0 + +version: '3.8' + +services: + app: + build: + context: . + dockerfile: Dockerfile + container_name: {{projectName}} + restart: unless-stopped + ports: + - "{{port}}:{{port}}" + environment: + - NODE_ENV=production + - PORT={{port}} + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:{{port}}{{healthCheckEndpoint}}"] + interval: 30s + timeout: 3s + retries: 3 + start_period: 10s + networks: + - app-network + +networks: + app-network: + driver: bridge diff --git a/cli-templates/kubernetes/configmap.yml.tpl b/cli-templates/kubernetes/configmap.yml.tpl new file mode 100644 index 0000000..5d5fea7 --- /dev/null +++ b/cli-templates/kubernetes/configmap.yml.tpl @@ -0,0 +1,17 @@ +# Kubernetes ConfigMap +# Generated by ExpressoTS CLI +# Template Version: 1.0.0 + +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{appName}}-config + namespace: {{namespace}} + labels: + app: {{appName}} +data: + NODE_ENV: "production" + PORT: "{{port}}" + # Add your application configuration here + # LOG_LEVEL: "info" + # API_VERSION: "v1" diff --git a/cli-templates/kubernetes/deployment.yml.tpl b/cli-templates/kubernetes/deployment.yml.tpl new file mode 100644 index 0000000..9ad5f69 --- /dev/null +++ b/cli-templates/kubernetes/deployment.yml.tpl @@ -0,0 +1,54 @@ +# Kubernetes Deployment +# Generated by ExpressoTS CLI +# Template Version: 1.0.0 + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{appName}} + namespace: {{namespace}} + labels: + app: {{appName}} +spec: + replicas: {{replicas}} + selector: + matchLabels: + app: {{appName}} + template: + metadata: + labels: + app: {{appName}} + spec: + containers: + - name: app + image: your-registry/{{appName}}:latest + ports: + - containerPort: {{port}} + name: http + env: + - name: NODE_ENV + value: "production" + - name: PORT + value: "{{port}}" + envFrom: + - configMapRef: + name: {{appName}}-config + resources: + requests: + memory: "{{memory}}" + cpu: "{{cpu}}" + limits: + memory: "512Mi" + cpu: "500m" + livenessProbe: + httpGet: + path: {{healthCheckPath}} + port: {{port}} + initialDelaySeconds: 30 + periodSeconds: 10 + readinessProbe: + httpGet: + path: {{healthCheckPath}} + port: {{port}} + initialDelaySeconds: 5 + periodSeconds: 5 diff --git a/cli-templates/kubernetes/ingress.yml.tpl b/cli-templates/kubernetes/ingress.yml.tpl new file mode 100644 index 0000000..8792083 --- /dev/null +++ b/cli-templates/kubernetes/ingress.yml.tpl @@ -0,0 +1,30 @@ +# Kubernetes Ingress +# Generated by ExpressoTS CLI +# Template Version: 1.0.0 + +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{appName}}-ingress + namespace: {{namespace}} + labels: + app: {{appName}} + annotations: + kubernetes.io/ingress.class: nginx + cert-manager.io/cluster-issuer: letsencrypt-prod +spec: + tls: + - hosts: + - {{appName}}.example.com + secretName: {{appName}}-tls + rules: + - host: {{appName}}.example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: {{appName}} + port: + number: 80 diff --git a/cli-templates/kubernetes/service.yml.tpl b/cli-templates/kubernetes/service.yml.tpl new file mode 100644 index 0000000..fc51f9e --- /dev/null +++ b/cli-templates/kubernetes/service.yml.tpl @@ -0,0 +1,20 @@ +# Kubernetes Service +# Generated by ExpressoTS CLI +# Template Version: 1.0.0 + +apiVersion: v1 +kind: Service +metadata: + name: {{appName}} + namespace: {{namespace}} + labels: + app: {{appName}} +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: {{port}} + protocol: TCP + name: http + selector: + app: {{appName}} diff --git a/cli-templates/manifest.json b/cli-templates/manifest.json new file mode 100644 index 0000000..edb3ac8 --- /dev/null +++ b/cli-templates/manifest.json @@ -0,0 +1,147 @@ +{ + "$schema": "https://raw.githubusercontent.com/expressots/templates/main/manifest.schema.json", + "version": "1.0.0", + "updated": "2026-01-07T00:00:00Z", + "description": "ExpressoTS CLI Templates Repository", + "templates": { + "cicd": { + "github": { + "basic": { + "path": "cicd/github/basic.yml", + "version": "1.0.0", + "description": "Basic GitHub Actions CI/CD pipeline with lint, test, and build" + }, + "comprehensive": { + "path": "cicd/github/comprehensive.yml", + "version": "1.0.0", + "description": "Full CI/CD pipeline with security scanning, E2E tests, and Docker" + }, + "security-focused": { + "path": "cicd/github/security-focused.yml", + "version": "1.0.0", + "description": "Security-focused pipeline with Trivy, Snyk, and OWASP checks" + } + }, + "gitlab": { + "basic": { + "path": "cicd/gitlab/basic.yml", + "version": "1.0.0", + "description": "Basic GitLab CI pipeline" + }, + "comprehensive": { + "path": "cicd/gitlab/comprehensive.yml", + "version": "1.0.0", + "description": "Comprehensive GitLab CI pipeline" + }, + "security-focused": { + "path": "cicd/gitlab/security-focused.yml", + "version": "1.0.0", + "description": "Security-focused GitLab CI pipeline" + } + }, + "circleci": { + "basic": { + "path": "cicd/circleci/basic.yml", + "version": "1.0.0", + "description": "Basic CircleCI pipeline" + }, + "comprehensive": { + "path": "cicd/circleci/comprehensive.yml", + "version": "1.0.0", + "description": "Comprehensive CircleCI pipeline" + } + }, + "jenkins": { + "basic": { + "path": "cicd/jenkins/basic.groovy", + "version": "1.0.0", + "description": "Basic Jenkinsfile" + } + }, + "bitbucket": { + "basic": { + "path": "cicd/bitbucket/basic.yml", + "version": "1.0.0", + "description": "Basic Bitbucket Pipelines" + } + }, + "azure": { + "basic": { + "path": "cicd/azure/basic.yml", + "version": "1.0.0", + "description": "Basic Azure DevOps pipeline" + } + } + }, + "docker": { + "production": { + "path": "docker/Dockerfile.production.tpl", + "version": "1.0.0", + "description": "Production-optimized multi-stage Dockerfile" + }, + "development": { + "path": "docker/Dockerfile.development.tpl", + "version": "1.0.0", + "description": "Development Dockerfile with hot reload" + }, + "compose": { + "path": "docker/docker-compose.yml.tpl", + "version": "1.0.0", + "description": "Production Docker Compose configuration" + }, + "compose-development": { + "path": "docker/docker-compose.development.yml.tpl", + "version": "1.0.0", + "description": "Development Docker Compose with volume mounts" + } + }, + "kubernetes": { + "deployment": { + "path": "kubernetes/deployment.yml.tpl", + "version": "1.0.0", + "description": "Kubernetes Deployment manifest" + }, + "service": { + "path": "kubernetes/service.yml.tpl", + "version": "1.0.0", + "description": "Kubernetes Service manifest" + }, + "configmap": { + "path": "kubernetes/configmap.yml.tpl", + "version": "1.0.0", + "description": "Kubernetes ConfigMap manifest" + }, + "ingress": { + "path": "kubernetes/ingress.yml.tpl", + "version": "1.0.0", + "description": "Kubernetes Ingress manifest" + } + }, + "migrations": { + "heroku-to-railway": { + "checklist": { + "path": "migrations/heroku-to-railway/checklist.md.tpl", + "version": "1.0.0" + } + }, + "heroku-to-render": { + "checklist": { + "path": "migrations/heroku-to-render/checklist.md.tpl", + "version": "1.0.0" + } + }, + "heroku-to-fly": { + "checklist": { + "path": "migrations/heroku-to-fly/checklist.md.tpl", + "version": "1.0.0" + } + }, + "compose-to-kubernetes": { + "checklist": { + "path": "migrations/compose-to-kubernetes/checklist.md.tpl", + "version": "1.0.0" + } + } + } + } +} diff --git a/cli-templates/migrations/compose-to-kubernetes/checklist.md.tpl b/cli-templates/migrations/compose-to-kubernetes/checklist.md.tpl new file mode 100644 index 0000000..68b01fb --- /dev/null +++ b/cli-templates/migrations/compose-to-kubernetes/checklist.md.tpl @@ -0,0 +1,48 @@ +# Docker Compose to Kubernetes Migration Checklist + +Generated by ExpressoTS CLI + +## Pre-Migration + +- [ ] Review existing docker-compose.yml +- [ ] Identify all services and dependencies +- [ ] Set up Kubernetes cluster (local or cloud) +- [ ] Install kubectl and configure context +- [ ] Create container registry access + +## Migration Steps + +### 1. Create Kubernetes Manifests +- [ ] Create namespace +- [ ] Create ConfigMaps for configuration +- [ ] Create Secrets for sensitive data +- [ ] Create Deployments for each service +- [ ] Create Services for networking +- [ ] Create Ingress for external access + +### 2. Container Images +- [ ] Push images to container registry +- [ ] Update image references in manifests +- [ ] Configure image pull secrets if needed + +### 3. Persistent Storage +- [ ] Create PersistentVolumeClaims +- [ ] Migrate data volumes +- [ ] Update pod specs with volume mounts + +### 4. Networking +- [ ] Configure service discovery +- [ ] Set up internal DNS +- [ ] Configure load balancing + +### 5. Deploy +- [ ] Apply manifests: `kubectl apply -f k8s/` +- [ ] Verify pods are running +- [ ] Check logs for errors + +## Post-Migration + +- [ ] Set up monitoring (Prometheus/Grafana) +- [ ] Configure alerting +- [ ] Set up CI/CD for Kubernetes +- [ ] Document runbooks diff --git a/cli-templates/migrations/heroku-to-fly/checklist.md.tpl b/cli-templates/migrations/heroku-to-fly/checklist.md.tpl new file mode 100644 index 0000000..b6e759a --- /dev/null +++ b/cli-templates/migrations/heroku-to-fly/checklist.md.tpl @@ -0,0 +1,41 @@ +# Heroku to Fly.io Migration Checklist + +Generated by ExpressoTS CLI + +## Pre-Migration + +- [ ] Export Heroku environment variables +- [ ] Install Fly CLI: `brew install flyctl` or `curl -L https://fly.io/install.sh | sh` +- [ ] Login to Fly: `flyctl auth login` +- [ ] Backup database if applicable + +## Migration Steps + +### 1. Initialize Fly App +- [ ] Run `flyctl launch` in project directory +- [ ] Review generated `fly.toml` +- [ ] Configure regions + +### 2. Environment Variables +- [ ] Set secrets: `flyctl secrets set KEY=value` +- [ ] Review `[env]` section in fly.toml + +### 3. Database Migration (if applicable) +- [ ] Create Fly Postgres: `flyctl postgres create` +- [ ] Attach to app: `flyctl postgres attach` +- [ ] Migrate data + +### 4. Deploy +- [ ] Deploy: `flyctl deploy` +- [ ] Check status: `flyctl status` +- [ ] View logs: `flyctl logs` + +### 5. DNS & Scaling +- [ ] Configure custom domain +- [ ] Scale as needed: `flyctl scale count 2` + +## Post-Migration + +- [ ] Verify all features +- [ ] Update CI/CD to use Fly +- [ ] Delete Heroku app diff --git a/cli-templates/migrations/heroku-to-railway/checklist.md.tpl b/cli-templates/migrations/heroku-to-railway/checklist.md.tpl new file mode 100644 index 0000000..3b71578 --- /dev/null +++ b/cli-templates/migrations/heroku-to-railway/checklist.md.tpl @@ -0,0 +1,54 @@ +# Heroku to Railway Migration Checklist + +Generated by ExpressoTS CLI + +## Pre-Migration + +- [ ] Export Heroku environment variables: `heroku config -a ` +- [ ] Export Heroku add-ons list: `heroku addons -a ` +- [ ] Backup database if applicable +- [ ] Review current Heroku dyno configuration +- [ ] Create Railway account at https://railway.app + +## Migration Steps + +### 1. Environment Setup +- [ ] Create new Railway project +- [ ] Link GitHub repository +- [ ] Configure environment variables in Railway dashboard + +### 2. Database Migration (if applicable) +- [ ] Provision Railway database +- [ ] Update DATABASE_URL in Railway +- [ ] Migrate data from Heroku database + +### 3. Deploy Configuration +- [ ] Review `railway.json` configuration +- [ ] Configure build settings +- [ ] Set start command + +### 4. Testing +- [ ] Deploy to Railway staging +- [ ] Verify application starts correctly +- [ ] Test all endpoints +- [ ] Check logs for errors + +### 5. DNS & Cutover +- [ ] Configure custom domain in Railway +- [ ] Update DNS records +- [ ] Monitor application health + +## Post-Migration + +- [ ] Verify all features work correctly +- [ ] Update CI/CD pipelines +- [ ] Update documentation +- [ ] Scale down/delete Heroku app + +## Environment Variable Mapping + +| Heroku | Railway | +|--------|---------| +| PORT | PORT (auto-configured) | +| DATABASE_URL | DATABASE_URL | +| NODE_ENV | NODE_ENV | diff --git a/cli-templates/migrations/heroku-to-render/checklist.md.tpl b/cli-templates/migrations/heroku-to-render/checklist.md.tpl new file mode 100644 index 0000000..13d6b28 --- /dev/null +++ b/cli-templates/migrations/heroku-to-render/checklist.md.tpl @@ -0,0 +1,42 @@ +# Heroku to Render Migration Checklist + +Generated by ExpressoTS CLI + +## Pre-Migration + +- [ ] Export Heroku environment variables +- [ ] Export Heroku add-ons list +- [ ] Backup database if applicable +- [ ] Create Render account at https://render.com + +## Migration Steps + +### 1. Create Render Service +- [ ] Create new Web Service in Render +- [ ] Connect GitHub/GitLab repository +- [ ] Configure build command +- [ ] Configure start command + +### 2. Environment Variables +- [ ] Add all environment variables +- [ ] Configure any secret groups + +### 3. Database Migration (if applicable) +- [ ] Create PostgreSQL database in Render +- [ ] Update DATABASE_URL +- [ ] Migrate data + +### 4. Testing +- [ ] Deploy to Render +- [ ] Verify application health +- [ ] Test all endpoints + +### 5. DNS Cutover +- [ ] Configure custom domain +- [ ] Update DNS records + +## Post-Migration + +- [ ] Verify all features +- [ ] Update CI/CD pipelines +- [ ] Delete Heroku app diff --git a/cli-templates/pricing.json b/cli-templates/pricing.json new file mode 100644 index 0000000..3c7a1dc --- /dev/null +++ b/cli-templates/pricing.json @@ -0,0 +1,121 @@ +{ + "$schema": "https://raw.githubusercontent.com/expressots/pricing/main/pricing.schema.json", + "version": "1.0.0", + "updated": "2026-01-07T00:00:00Z", + "description": "ExpressoTS CLI Cloud Provider Pricing Data", + "disclaimer": "Prices are estimates and may vary by region. Check provider websites for current pricing.", + "providers": { + "aws": { + "serviceName": "ECS Fargate", + "model": "per-hour", + "basePrice": 0, + "cpuPerHour": 0.04048, + "memoryPerGbHour": 0.004445, + "storagePerGb": 0.10, + "bandwidthPerGb": 0.09, + "freeBandwidth": 100, + "freeCredits": 0, + "notes": "Prices for us-east-1 region", + "source": "https://aws.amazon.com/fargate/pricing/", + "lastVerified": "2026-01-05" + }, + "gcp": { + "serviceName": "Cloud Run", + "model": "per-hour", + "basePrice": 0, + "cpuPerHour": 0.024, + "memoryPerGbHour": 0.0025, + "storagePerGb": 0.10, + "bandwidthPerGb": 0.12, + "freeBandwidth": 200, + "freeCredits": 300, + "notes": "Pay only for what you use, scales to zero", + "source": "https://cloud.google.com/run/pricing", + "lastVerified": "2026-01-05" + }, + "azure": { + "serviceName": "Container Apps", + "model": "per-hour", + "basePrice": 0, + "cpuPerHour": 0.024, + "memoryPerGbHour": 0.003, + "storagePerGb": 0.10, + "bandwidthPerGb": 0.087, + "freeBandwidth": 100, + "freeCredits": 200, + "notes": "First 180,000 vCPU-seconds free per month", + "source": "https://azure.microsoft.com/pricing/details/container-apps/", + "lastVerified": "2026-01-05" + }, + "railway": { + "serviceName": "Web Service", + "model": "usage", + "basePrice": 5, + "cpuPerHour": 0.000463, + "memoryPerGbHour": 0.000231, + "storagePerGb": 0.25, + "bandwidthPerGb": 0, + "freeBandwidth": 1000, + "freeCredits": 5, + "notes": "Usage-based pricing, great DX", + "source": "https://railway.app/pricing", + "lastVerified": "2026-01-05" + }, + "render": { + "serviceName": "Web Service", + "model": "per-month", + "basePrice": 7, + "cpuPerHour": 0, + "memoryPerGbHour": 0, + "storagePerGb": 0.25, + "bandwidthPerGb": 0.10, + "freeBandwidth": 100, + "freeCredits": 0, + "notes": "Simple pricing, auto-scaling available", + "source": "https://render.com/pricing", + "lastVerified": "2026-01-05" + }, + "fly": { + "serviceName": "Machines", + "model": "per-hour", + "basePrice": 0, + "cpuPerHour": 0.0000158, + "memoryPerGbHour": 0.0000047, + "storagePerGb": 0.15, + "bandwidthPerGb": 0.02, + "freeBandwidth": 100, + "freeCredits": 0, + "notes": "Pay for resources while running, scales to zero", + "source": "https://fly.io/docs/about/pricing/", + "lastVerified": "2026-01-05" + }, + "digitalocean": { + "serviceName": "App Platform", + "model": "per-month", + "basePrice": 5, + "cpuPerHour": 0, + "memoryPerGbHour": 0, + "storagePerGb": 0.10, + "bandwidthPerGb": 0.01, + "freeBandwidth": 500, + "freeCredits": 0, + "notes": "Simple pricing, good for small projects", + "source": "https://www.digitalocean.com/pricing/app-platform", + "lastVerified": "2026-01-05" + }, + "heroku": { + "serviceName": "Eco Dyno", + "model": "per-month", + "basePrice": 5, + "cpuPerHour": 0, + "memoryPerGbHour": 0, + "storagePerGb": 0, + "bandwidthPerGb": 0, + "freeBandwidth": 2000, + "freeCredits": 0, + "notes": "Basic: $7/mo, Standard: $25/mo, Performance: $250+/mo", + "source": "https://www.heroku.com/pricing", + "lastVerified": "2026-01-05" + } + } +} From 44b1606fd7f6321b8cdf9f946445a84f63ff478e Mon Sep 17 00:00:00 2001 From: Richard Zampieri Date: Sun, 11 Jan 2026 01:24:20 -0800 Subject: [PATCH 14/21] feat: add Docker and Kubernetes configurations for ExpressoTS Micro Template - Introduced Docker Compose setup for local development and production environments. - Added multi-stage Dockerfile for optimized production builds. - Created Kubernetes manifests including ConfigMap, Deployment, Service, Ingress, and Network Policy for production readiness. - Implemented observability features with health checks and metrics endpoints. - Provided example implementations for dependency injection, event-driven architecture, and observability in the microservice context. --- micro/Dockerfile | 57 +++ micro/docker-compose.yml | 173 +++++++++ micro/env.example | 33 ++ micro/k8s/configmap.yml | 59 +++ micro/k8s/deployment.yml | 191 ++++++++++ micro/k8s/ingress.yml | 105 ++++++ micro/src/api.ts | 135 ++++++- micro/src/examples/di-container.example.ts | 265 +++++++++++++ micro/src/examples/event-driven.example.ts | 204 ++++++++++ micro/src/examples/observability.example.ts | 348 ++++++++++++++++++ .../src/examples/serverless-lambda.example.ts | 146 ++++++++ 11 files changed, 1709 insertions(+), 7 deletions(-) create mode 100644 micro/Dockerfile create mode 100644 micro/docker-compose.yml create mode 100644 micro/env.example create mode 100644 micro/k8s/configmap.yml create mode 100644 micro/k8s/deployment.yml create mode 100644 micro/k8s/ingress.yml create mode 100644 micro/src/examples/di-container.example.ts create mode 100644 micro/src/examples/event-driven.example.ts create mode 100644 micro/src/examples/observability.example.ts create mode 100644 micro/src/examples/serverless-lambda.example.ts diff --git a/micro/Dockerfile b/micro/Dockerfile new file mode 100644 index 0000000..59a2094 --- /dev/null +++ b/micro/Dockerfile @@ -0,0 +1,57 @@ +# ExpressoTS Micro Template - Production Dockerfile +# Multi-stage build for optimal image size + +# ============================================================================ +# Build Stage +# ============================================================================ +FROM node:20-alpine AS builder + +# Set working directory +WORKDIR /app + +# Install dependencies first (for better caching) +COPY package*.json ./ +RUN npm ci --only=production && npm cache clean --force + +# Copy source files +COPY . . + +# Build TypeScript +RUN npm run build + +# Remove dev dependencies +RUN npm prune --production + +# ============================================================================ +# Production Stage +# ============================================================================ +FROM node:20-alpine AS production + +# Set working directory +WORKDIR /app + +# Create non-root user for security +RUN addgroup -g 1001 -S nodejs && \ + adduser -S expressots -u 1001 -G nodejs + +# Copy built files from builder +COPY --from=builder --chown=expressots:nodejs /app/node_modules ./node_modules +COPY --from=builder --chown=expressots:nodejs /app/dist ./dist +COPY --from=builder --chown=expressots:nodejs /app/package.json ./ + +# Set environment variables +ENV NODE_ENV=production +ENV PORT=3000 + +# Switch to non-root user +USER expressots + +# Expose port +EXPOSE 3000 + +# Health check for container orchestration +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1 + +# Start the application +CMD ["node", "dist/src/api.js"] diff --git a/micro/docker-compose.yml b/micro/docker-compose.yml new file mode 100644 index 0000000..ae979fb --- /dev/null +++ b/micro/docker-compose.yml @@ -0,0 +1,173 @@ +# ExpressoTS Micro Template - Docker Compose +# For local development and testing + +version: "3.8" + +services: + # Main application + app: + build: + context: . + dockerfile: Dockerfile + ports: + - "3000:3000" + environment: + - NODE_ENV=production + - PORT=3000 + healthcheck: + test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/health"] + interval: 30s + timeout: 3s + retries: 3 + start_period: 5s + restart: unless-stopped + + # Development mode with hot reload + app-dev: + build: + context: . + dockerfile: Dockerfile.development + ports: + - "3000:3000" + environment: + - NODE_ENV=development + - PORT=3000 + volumes: + - ./src:/app/src:ro + - ./package.json:/app/package.json:ro + command: npm run dev + profiles: + - dev + +# Optional services for development (activate with profile) + + # Redis for caching/sessions + redis: + image: redis:7-alpine + ports: + - "6379:6379" + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 3s + retries: 3 + profiles: + - with-redis + + # PostgreSQL database + postgres: + image: postgres:16-alpine + ports: + - "5432:5432" + environment: + - POSTGRES_USER=expressots + - POSTGRES_PASSWORD=expressots + - POSTGRES_DB=expressots + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U expressots"] + interval: 10s + timeout: 3s + retries: 3 + profiles: + - with-postgres + + # MongoDB database + mongo: + image: mongo:7 + ports: + - "27017:27017" + environment: + - MONGO_INITDB_ROOT_USERNAME=expressots + - MONGO_INITDB_ROOT_PASSWORD=expressots + volumes: + - mongo_data:/data/db + profiles: + - with-mongo + + # RabbitMQ for message queuing + rabbitmq: + image: rabbitmq:3-management-alpine + ports: + - "5672:5672" + - "15672:15672" + environment: + - RABBITMQ_DEFAULT_USER=expressots + - RABBITMQ_DEFAULT_PASS=expressots + volumes: + - rabbitmq_data:/var/lib/rabbitmq + healthcheck: + test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"] + interval: 10s + timeout: 3s + retries: 3 + profiles: + - with-rabbitmq + + # Prometheus for metrics + prometheus: + image: prom/prometheus:latest + ports: + - "9090:9090" + volumes: + - ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro + command: + - "--config.file=/etc/prometheus/prometheus.yml" + profiles: + - with-monitoring + + # Grafana for dashboards + grafana: + image: grafana/grafana:latest + ports: + - "3001:3000" + environment: + - GF_SECURITY_ADMIN_PASSWORD=admin + volumes: + - grafana_data:/var/lib/grafana + profiles: + - with-monitoring + + # Jaeger for distributed tracing + jaeger: + image: jaegertracing/all-in-one:latest + ports: + - "16686:16686" + - "14268:14268" + environment: + - COLLECTOR_OTLP_ENABLED=true + profiles: + - with-tracing + +volumes: + redis_data: + postgres_data: + mongo_data: + rabbitmq_data: + grafana_data: + +# Usage: +# +# Production mode: +# docker-compose up app +# +# Development mode: +# docker-compose --profile dev up app-dev +# +# With Redis: +# docker-compose --profile with-redis up app redis +# +# With PostgreSQL: +# docker-compose --profile with-postgres up app postgres +# +# With Monitoring (Prometheus + Grafana): +# docker-compose --profile with-monitoring up app prometheus grafana +# +# With Tracing (Jaeger): +# docker-compose --profile with-tracing up app jaeger +# +# Full stack: +# docker-compose --profile with-redis --profile with-postgres --profile with-monitoring --profile with-tracing up diff --git a/micro/env.example b/micro/env.example new file mode 100644 index 0000000..f984b9a --- /dev/null +++ b/micro/env.example @@ -0,0 +1,33 @@ +# ExpressoTS Micro Template - Environment Configuration +# Copy this file to .env.local for development + +# Application +APP_NAME=\ ExpressoTS Micro\ +APP_VERSION=\1.0.0\ +NODE_ENV=\development\ + +# Server +PORT=3000 +HOST=\0.0.0.0\ + +# Logging +LOG_LEVEL=\debug\ +LOG_FORMAT=\pretty\ + +# Database (uncomment as needed) +# DATABASE_URL=\postgresql://user:password@localhost:5432/expressots\ + +# Redis (uncomment as needed) +# REDIS_URL=\redis://localhost:6379\ + +# Authentication (uncomment as needed) +# JWT_SECRET=\your-super-secret-jwt-key\ +# JWT_EXPIRES_IN=\7d\ + +# Observability (uncomment as needed) +# JAEGER_ENDPOINT=\http://localhost:14268/api/traces\ +# TRACING_ENABLED=\true\ + +# Message Queue (uncomment as needed) +# RABBITMQ_URL=\amqp://guest:guest@localhost:5672\ + diff --git a/micro/k8s/configmap.yml b/micro/k8s/configmap.yml new file mode 100644 index 0000000..5963c09 --- /dev/null +++ b/micro/k8s/configmap.yml @@ -0,0 +1,59 @@ +# ExpressoTS Micro Template - Kubernetes ConfigMap +# Non-sensitive configuration values + +apiVersion: v1 +kind: ConfigMap +metadata: + name: expressots-micro-config + labels: + app: expressots-micro +data: + # Application settings + APP_NAME: "ExpressoTS Micro" + APP_VERSION: "1.0.0" + NODE_ENV: "production" + PORT: "3000" + + # Logging + LOG_LEVEL: "info" + LOG_FORMAT: "json" + + # Performance + NODE_OPTIONS: "--max-old-space-size=256" + + # Observability (optional - uncomment as needed) + # JAEGER_ENDPOINT: "http://jaeger-collector:14268/api/traces" + # PROMETHEUS_ENABLED: "true" + # TRACING_ENABLED: "true" + # TRACING_SAMPLE_RATE: "1.0" + + # Service Discovery (optional - uncomment as needed) + # CONSUL_URL: "http://consul:8500" + # SERVICE_DISCOVERY_ENABLED: "true" + +--- +# Secret for sensitive values +# In production, use external secret management (Vault, AWS Secrets Manager, etc.) +apiVersion: v1 +kind: Secret +metadata: + name: expressots-micro-secrets + labels: + app: expressots-micro +type: Opaque +stringData: + # Database (if using) + # DATABASE_URL: "postgresql://user:password@postgres:5432/db" + + # Redis (if using) + # REDIS_URL: "redis://redis:6379" + + # JWT (if using auth) + # JWT_SECRET: "your-super-secret-jwt-key" + + # API Keys (if using external services) + # STRIPE_SECRET_KEY: "sk_live_..." + # SENDGRID_API_KEY: "SG..." + + # Placeholder - replace with actual secrets + PLACEHOLDER: "replace-with-real-secrets" diff --git a/micro/k8s/deployment.yml b/micro/k8s/deployment.yml new file mode 100644 index 0000000..0f28540 --- /dev/null +++ b/micro/k8s/deployment.yml @@ -0,0 +1,191 @@ +# ExpressoTS Micro Template - Kubernetes Deployment +# Production-ready configuration with health checks, resources, and scaling + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: expressots-micro + labels: + app: expressots-micro + version: v1 +spec: + replicas: 3 + selector: + matchLabels: + app: expressots-micro + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + template: + metadata: + labels: + app: expressots-micro + version: v1 + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "3000" + prometheus.io/path: "/metrics" + spec: + serviceAccountName: expressots-micro + securityContext: + runAsNonRoot: true + runAsUser: 1001 + runAsGroup: 1001 + fsGroup: 1001 + containers: + - name: app + image: expressots-micro:latest + imagePullPolicy: Always + ports: + - name: http + containerPort: 3000 + protocol: TCP + env: + - name: NODE_ENV + value: "production" + - name: PORT + value: "3000" + envFrom: + - configMapRef: + name: expressots-micro-config + optional: true + - secretRef: + name: expressots-micro-secrets + optional: true + resources: + requests: + memory: "128Mi" + cpu: "100m" + limits: + memory: "256Mi" + cpu: "500m" + livenessProbe: + httpGet: + path: /health/live + port: 3000 + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /health/ready + port: 3000 + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 3 + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchExpressions: + - key: app + operator: In + values: + - expressots-micro + topologyKey: kubernetes.io/hostname + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: topology.kubernetes.io/zone + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app: expressots-micro + +--- +# Service +apiVersion: v1 +kind: Service +metadata: + name: expressots-micro + labels: + app: expressots-micro +spec: + type: ClusterIP + ports: + - name: http + port: 80 + targetPort: 3000 + protocol: TCP + selector: + app: expressots-micro + +--- +# Service Account +apiVersion: v1 +kind: ServiceAccount +metadata: + name: expressots-micro + labels: + app: expressots-micro + +--- +# Horizontal Pod Autoscaler +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: expressots-micro + labels: + app: expressots-micro +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: expressots-micro + minReplicas: 3 + maxReplicas: 10 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 70 + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: 80 + behavior: + scaleDown: + stabilizationWindowSeconds: 300 + policies: + - type: Percent + value: 10 + periodSeconds: 60 + scaleUp: + stabilizationWindowSeconds: 0 + policies: + - type: Percent + value: 100 + periodSeconds: 15 + - type: Pods + value: 4 + periodSeconds: 15 + selectPolicy: Max + +--- +# Pod Disruption Budget +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: expressots-micro + labels: + app: expressots-micro +spec: + minAvailable: 2 + selector: + matchLabels: + app: expressots-micro diff --git a/micro/k8s/ingress.yml b/micro/k8s/ingress.yml new file mode 100644 index 0000000..9d9ea32 --- /dev/null +++ b/micro/k8s/ingress.yml @@ -0,0 +1,105 @@ +# ExpressoTS Micro Template - Kubernetes Ingress +# HTTPS configuration with TLS termination + +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: expressots-micro + labels: + app: expressots-micro + annotations: + # Ingress class + kubernetes.io/ingress.class: "nginx" + + # TLS/SSL with cert-manager + cert-manager.io/cluster-issuer: "letsencrypt-prod" + + # Rate limiting + nginx.ingress.kubernetes.io/rate-limit: "100" + nginx.ingress.kubernetes.io/rate-limit-window: "1m" + + # Timeouts + nginx.ingress.kubernetes.io/proxy-connect-timeout: "30" + nginx.ingress.kubernetes.io/proxy-read-timeout: "30" + nginx.ingress.kubernetes.io/proxy-send-timeout: "30" + + # Body size limit + nginx.ingress.kubernetes.io/proxy-body-size: "10m" + + # CORS (if needed) + # nginx.ingress.kubernetes.io/enable-cors: "true" + # nginx.ingress.kubernetes.io/cors-allow-origin: "*" + + # Security headers + nginx.ingress.kubernetes.io/configuration-snippet: | + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; +spec: + ingressClassName: nginx + tls: + - hosts: + - api.example.com + secretName: expressots-micro-tls + rules: + - host: api.example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: expressots-micro + port: + number: 80 + +--- +# Network Policy - Restrict traffic +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: expressots-micro + labels: + app: expressots-micro +spec: + podSelector: + matchLabels: + app: expressots-micro + policyTypes: + - Ingress + - Egress + ingress: + # Allow traffic from ingress controller + - from: + - namespaceSelector: + matchLabels: + name: ingress-nginx + ports: + - protocol: TCP + port: 3000 + # Allow traffic from prometheus for scraping + - from: + - namespaceSelector: + matchLabels: + name: monitoring + ports: + - protocol: TCP + port: 3000 + egress: + # Allow DNS + - to: + - namespaceSelector: {} + ports: + - protocol: UDP + port: 53 + # Allow outbound HTTPS + - to: + - ipBlock: + cidr: 0.0.0.0/0 + ports: + - protocol: TCP + port: 443 + # Allow outbound to other services in same namespace + - to: + - podSelector: {} diff --git a/micro/src/api.ts b/micro/src/api.ts index 5da085e..7ad0915 100644 --- a/micro/src/api.ts +++ b/micro/src/api.ts @@ -2,10 +2,18 @@ import { createMicroAPI } from "@expressots/adapter-express"; import { defineConfig, Env, loadEnvSync } from "@expressots/core"; import { Request, Response } from "express"; -// Environment configuration -loadEnvSync({ files: { development: ".env.local", production: ".env.prod" } }); +// ============================================================================ +// Environment Configuration +// ============================================================================ + +loadEnvSync({ + files: { development: ".env.local", production: ".env.prod" }, +}); + +// ============================================================================ +// Type-Safe Configuration +// ============================================================================ -// Type-safe configuration const config = defineConfig({ app: { name: Env.string("APP_NAME", { default: "ExpressoTS Micro" }), @@ -16,28 +24,141 @@ const config = defineConfig({ }, }); -// Create micro API +// ============================================================================ +// Create Micro API +// ============================================================================ + const microAPI = createMicroAPI(); + +// ============================================================================ +// Optional: Register Services in DI Container +// ============================================================================ + +// Example service registration (uncomment to use): +// +// class UserService { +// findAll() { +// return [{ id: 1, name: "John Doe" }]; +// } +// findById(id: string) { +// return { id, name: "John Doe", email: "john@example.com" }; +// } +// } +// +// microAPI.Container.addSingleton(UserService); + +// ============================================================================ +// Build Application +// ============================================================================ + const app = microAPI.build(); +// ============================================================================ +// Middleware Configuration (V4 Unified Methods) +// ============================================================================ + +// Request parsing (JSON + URL-encoded) +app.Middleware.parse(); + +// Security (CORS enabled) +app.Middleware.security({ cors: true }); + +// Error handler +app.Middleware.setErrorHandler({ + showStackTrace: process.env.NODE_ENV === "development", +}); + +// ============================================================================ // Routes +// ============================================================================ + +// Root endpoint - Service info app.Route.get("/", (req: Request, res: Response) => { res.json({ name: config.values.app.name, version: config.values.app.version, message: "Hello from ExpressoTS Micro API!", + timestamp: new Date().toISOString(), }); }); +// Health check - K8s compatible app.Route.get("/health", (req: Request, res: Response) => { res.json({ status: "healthy", timestamp: new Date().toISOString(), + uptime: process.uptime(), + environment: process.env.NODE_ENV || "development", + }); +}); + +// Kubernetes readiness probe +app.Route.get("/health/ready", (req: Request, res: Response) => { + // Add custom readiness checks here + // e.g., database connection, external services + const isReady = true; + + res.status(isReady ? 200 : 503).json({ + status: isReady ? "ready" : "not ready", + timestamp: new Date().toISOString(), + }); +}); + +// Kubernetes liveness probe +app.Route.get("/health/live", (req: Request, res: Response) => { + res.json({ + status: "alive", + timestamp: new Date().toISOString(), }); }); -// Start server +// ============================================================================ +// Example Routes (Uncomment to use) +// ============================================================================ + +// Example: GET /users - Using DI container +// app.Route.get("/users", (req: Request, res: Response) => { +// const userService = microAPI.Container.get(UserService); +// const users = userService.findAll(); +// res.json(users); +// }); + +// Example: GET /users/:id - Route parameters +// app.Route.get("/users/:id", (req: Request, res: Response) => { +// const userService = microAPI.Container.get(UserService); +// const user = userService.findById(req.params.id); +// if (!user) { +// return res.status(404).json({ error: "User not found" }); +// } +// res.json(user); +// }); + +// Example: POST /users - With request body +// app.Route.post("/users", (req: Request, res: Response) => { +// const { name, email } = req.body; +// if (!name || !email) { +// return res.status(400).json({ error: "Name and email required" }); +// } +// const user = { id: Date.now().toString(), name, email }; +// res.status(201).json(user); +// }); + +// ============================================================================ +// Start Server +// ============================================================================ + const port = config.values.server.port; -app.listen(port, () => { - console.log(`๐Ÿš€ Micro API running on http://localhost:${port}`); + +app.listen(port, { + appName: config.values.app.name, + appVersion: config.values.app.version, }); + +console.log(` +๐Ÿš€ ${config.values.app.name} v${config.values.app.version} + + Server: http://localhost:${port} + Health: http://localhost:${port}/health + Ready: http://localhost:${port}/health/ready + Live: http://localhost:${port}/health/live +`); diff --git a/micro/src/examples/di-container.example.ts b/micro/src/examples/di-container.example.ts new file mode 100644 index 0000000..aff2a94 --- /dev/null +++ b/micro/src/examples/di-container.example.ts @@ -0,0 +1,265 @@ +/** + * Dependency Injection Container Example + * + * This example demonstrates how to use the DI container in the + * ExpressoTS micro template for service registration and resolution. + */ + +import { createMicroAPI } from "@expressots/adapter-express"; +import { defineConfig, Env, loadEnvSync } from "@expressots/core"; +import { Request, Response } from "express"; + +// Load environment +loadEnvSync({ files: { development: ".env.local" } }); + +// Configuration +const config = defineConfig({ + app: { + name: Env.string("APP_NAME", { default: "DI Container Example" }), + }, + server: { + port: Env.number("PORT", { default: 3000 }), + }, +}); + +// ============================================================================ +// Service Definitions +// ============================================================================ + +// Repository - Data access layer +class UserRepository { + private users = [ + { id: "1", name: "John Doe", email: "john@example.com" }, + { id: "2", name: "Jane Smith", email: "jane@example.com" }, + { id: "3", name: "Bob Wilson", email: "bob@example.com" }, + ]; + + findAll() { + return this.users; + } + + findById(id: string) { + return this.users.find((u) => u.id === id); + } + + create(name: string, email: string) { + const user = { + id: Date.now().toString(), + name, + email, + }; + this.users.push(user); + return user; + } + + delete(id: string) { + const index = this.users.findIndex((u) => u.id === id); + if (index !== -1) { + this.users.splice(index, 1); + return true; + } + return false; + } +} + +// Service - Business logic layer +class UserService { + constructor(private userRepository: UserRepository) {} + + getAllUsers() { + return this.userRepository.findAll(); + } + + getUserById(id: string) { + const user = this.userRepository.findById(id); + if (!user) { + throw new Error(`User ${id} not found`); + } + return user; + } + + createUser(name: string, email: string) { + // Validation + if (!name || name.length < 2) { + throw new Error("Name must be at least 2 characters"); + } + if (!email || !email.includes("@")) { + throw new Error("Invalid email address"); + } + return this.userRepository.create(name, email); + } + + deleteUser(id: string) { + if (!this.userRepository.delete(id)) { + throw new Error(`User ${id} not found`); + } + return { success: true }; + } +} + +// Logger service +class LoggerService { + private serviceName: string; + + constructor(serviceName: string = "app") { + this.serviceName = serviceName; + } + + info(message: string, data?: Record) { + console.log( + JSON.stringify({ + timestamp: new Date().toISOString(), + level: "info", + service: this.serviceName, + message, + ...data, + }) + ); + } + + error(message: string, error?: Error) { + console.error( + JSON.stringify({ + timestamp: new Date().toISOString(), + level: "error", + service: this.serviceName, + message, + error: error?.message, + stack: error?.stack, + }) + ); + } +} + +// ============================================================================ +// Create Micro API & Register Services +// ============================================================================ + +const microAPI = createMicroAPI(); + +// Register services with different scopes + +// Singleton - Same instance for entire app lifetime +// Good for: Database connections, loggers, config +microAPI.Container.addSingleton(UserRepository); +microAPI.Container.addSingleton(LoggerService); + +// Transient - New instance every time +// Good for: Stateless services, request handlers +// Note: UserService depends on UserRepository, so we need to manually create it +// In a more complex setup, you'd use inversify's proper binding + +// Manual binding for services with dependencies +const userRepo = new UserRepository(); +const userService = new UserService(userRepo); +const logger = new LoggerService("user-service"); + +// Build application +const app = microAPI.build(); +app.Middleware.parse(); + +// ============================================================================ +// Routes using DI +// ============================================================================ + +app.Route.get("/", (req: Request, res: Response) => { + logger.info("Root endpoint accessed"); + res.json({ + name: config.values.app.name, + message: "DI Container Example", + endpoints: [ + "GET /users - List all users", + "GET /users/:id - Get user by ID", + "POST /users - Create user", + "DELETE /users/:id - Delete user", + ], + }); +}); + +// GET /users - List all users +app.Route.get("/users", (req: Request, res: Response) => { + logger.info("Listing all users"); + const users = userService.getAllUsers(); + res.json(users); +}); + +// GET /users/:id - Get user by ID +app.Route.get("/users/:id", (req: Request, res: Response) => { + const { id } = req.params; + logger.info("Getting user by ID", { userId: id }); + + try { + const user = userService.getUserById(id); + res.json(user); + } catch (error: any) { + logger.error("User not found", error); + res.status(404).json({ error: error.message }); + } +}); + +// POST /users - Create user +app.Route.post("/users", (req: Request, res: Response) => { + const { name, email } = req.body; + logger.info("Creating user", { name, email }); + + try { + const user = userService.createUser(name, email); + logger.info("User created", { userId: user.id }); + res.status(201).json(user); + } catch (error: any) { + logger.error("Failed to create user", error); + res.status(400).json({ error: error.message }); + } +}); + +// DELETE /users/:id - Delete user +app.Route.delete("/users/:id", (req: Request, res: Response) => { + const { id } = req.params; + logger.info("Deleting user", { userId: id }); + + try { + userService.deleteUser(id); + logger.info("User deleted", { userId: id }); + res.json({ message: `User ${id} deleted` }); + } catch (error: any) { + logger.error("Failed to delete user", error); + res.status(404).json({ error: error.message }); + } +}); + +// Health check +app.Route.get("/health", (req: Request, res: Response) => { + res.json({ + status: "healthy", + timestamp: new Date().toISOString(), + }); +}); + +// ============================================================================ +// Start Server +// ============================================================================ + +const port = config.values.server.port; +app.listen(port); + +console.log(` +๐Ÿš€ DI Container Example running on http://localhost:${port} + +Services registered: + - UserRepository (Singleton) + - UserService (uses UserRepository) + - LoggerService (Singleton) + +Endpoints: + GET / - API info + GET /users - List all users + GET /users/:id - Get user by ID + POST /users - Create user (body: { name, email }) + DELETE /users/:id - Delete user + GET /health - Health check + +Try: + curl http://localhost:${port}/users + curl http://localhost:${port}/users/1 + curl -X POST http://localhost:${port}/users -H "Content-Type: application/json" -d '{"name":"Alice","email":"alice@example.com"}' +`); diff --git a/micro/src/examples/event-driven.example.ts b/micro/src/examples/event-driven.example.ts new file mode 100644 index 0000000..286f79e --- /dev/null +++ b/micro/src/examples/event-driven.example.ts @@ -0,0 +1,204 @@ +/** + * Event-Driven Microservice Example + * + * This example demonstrates how to build an event-driven microservice + * using the ExpressoTS micro template with EventEmitter. + */ + +import { createMicroAPI } from "@expressots/adapter-express"; +import { EventEmitter, defineConfig, Env, loadEnvSync } from "@expressots/core"; +import { Request, Response } from "express"; + +// Load environment +loadEnvSync({ files: { development: ".env.local", production: ".env.prod" } }); + +// Configuration +const config = defineConfig({ + app: { + name: Env.string("APP_NAME", { default: "Event-Driven Micro" }), + }, + server: { + port: Env.number("PORT", { default: 3000 }), + }, +}); + +// Create micro API with EventEmitter +const microAPI = createMicroAPI(); +microAPI.Container.addSingleton(EventEmitter); + +const app = microAPI.build(); +const events = microAPI.Container.get(EventEmitter); + +app.Middleware.parse(); + +// ============================================================================ +// Event Handlers +// ============================================================================ + +// Order events +events.on("order.created", async (data: { orderId: string; userId: string }) => { + console.log(`[Event] Order created: ${data.orderId} by user ${data.userId}`); + // In a real app: + // - Send confirmation email + // - Update inventory + // - Notify warehouse +}); + +events.on("order.shipped", async (data: { orderId: string; trackingNumber: string }) => { + console.log(`[Event] Order shipped: ${data.orderId}, tracking: ${data.trackingNumber}`); + // In a real app: + // - Send shipping notification + // - Update order status +}); + +events.on("order.cancelled", async (data: { orderId: string; reason: string }) => { + console.log(`[Event] Order cancelled: ${data.orderId}, reason: ${data.reason}`); + // In a real app: + // - Process refund + // - Restore inventory + // - Send cancellation email +}); + +// User events +events.on("user.registered", async (data: { userId: string; email: string }) => { + console.log(`[Event] User registered: ${data.userId} (${data.email})`); + // In a real app: + // - Send welcome email + // - Create default settings + // - Track analytics +}); + +// ============================================================================ +// Routes +// ============================================================================ + +app.Route.get("/", (req: Request, res: Response) => { + res.json({ + name: config.values.app.name, + message: "Event-Driven Microservice Example", + events: ["order.created", "order.shipped", "order.cancelled", "user.registered"], + }); +}); + +// Create order - emits event +app.Route.post("/orders", async (req: Request, res: Response) => { + const { userId, items, total } = req.body; + + if (!userId || !items) { + return res.status(400).json({ error: "userId and items required" }); + } + + const order = { + id: `order_${Date.now()}`, + userId, + items, + total: total || 0, + status: "created", + createdAt: new Date().toISOString(), + }; + + // Emit event for async processing + await events.emit("order.created", { + orderId: order.id, + userId: order.userId, + }); + + res.status(201).json(order); +}); + +// Ship order - emits event +app.Route.post("/orders/:id/ship", async (req: Request, res: Response) => { + const { id } = req.params; + const { trackingNumber } = req.body; + + if (!trackingNumber) { + return res.status(400).json({ error: "trackingNumber required" }); + } + + // Emit event + await events.emit("order.shipped", { + orderId: id, + trackingNumber, + }); + + res.json({ + orderId: id, + status: "shipped", + trackingNumber, + }); +}); + +// Cancel order - emits event +app.Route.post("/orders/:id/cancel", async (req: Request, res: Response) => { + const { id } = req.params; + const { reason } = req.body; + + // Emit event + await events.emit("order.cancelled", { + orderId: id, + reason: reason || "No reason provided", + }); + + res.json({ + orderId: id, + status: "cancelled", + }); +}); + +// Register user - emits event +app.Route.post("/users", async (req: Request, res: Response) => { + const { email, name } = req.body; + + if (!email || !name) { + return res.status(400).json({ error: "email and name required" }); + } + + const user = { + id: `user_${Date.now()}`, + email, + name, + createdAt: new Date().toISOString(), + }; + + // Emit event + await events.emit("user.registered", { + userId: user.id, + email: user.email, + }); + + res.status(201).json(user); +}); + +// Event statistics +app.Route.get("/events/stats", (req: Request, res: Response) => { + res.json({ + registeredEvents: [ + "order.created", + "order.shipped", + "order.cancelled", + "user.registered", + ], + timestamp: new Date().toISOString(), + }); +}); + +// Health check +app.Route.get("/health", (req: Request, res: Response) => { + res.json({ status: "healthy", timestamp: new Date().toISOString() }); +}); + +// Start server +const port = config.values.server.port; +app.listen(port); + +console.log(` +๐Ÿš€ Event-Driven Microservice running on http://localhost:${port} + +Endpoints: + POST /orders - Create order (emits order.created) + POST /orders/:id/ship - Ship order (emits order.shipped) + POST /orders/:id/cancel - Cancel order (emits order.cancelled) + POST /users - Register user (emits user.registered) + GET /events/stats - Event statistics + GET /health - Health check +`); diff --git a/micro/src/examples/observability.example.ts b/micro/src/examples/observability.example.ts new file mode 100644 index 0000000..c470b59 --- /dev/null +++ b/micro/src/examples/observability.example.ts @@ -0,0 +1,348 @@ +/** + * Observability Example + * + * This example demonstrates how to implement production-ready + * observability in the ExpressoTS micro template including: + * - Health checks (K8s ready) + * - Prometheus metrics + * - Structured logging + * - Request tracing + */ + +import { createMicroAPI } from "@expressots/adapter-express"; +import { defineConfig, Env, loadEnvSync } from "@expressots/core"; +import { Request, Response, NextFunction } from "express"; + +// Load environment +loadEnvSync({ files: { development: ".env.local" } }); + +// Configuration +const config = defineConfig({ + app: { + name: Env.string("APP_NAME", { default: "Observability Example" }), + version: Env.string("APP_VERSION", { default: "1.0.0" }), + }, + server: { + port: Env.number("PORT", { default: 3000 }), + }, +}); + +// ============================================================================ +// Observability Providers (Simplified versions - use @expressots/micro-providers in production) +// ============================================================================ + +// Health Check Provider +class HealthCheckProvider { + private checks: Map Promise> = new Map(); + + addCheck(name: string, check: () => Promise) { + this.checks.set(name, check); + } + + async getHealth() { + const results: Record = {}; + let allHealthy = true; + + for (const [name, check] of this.checks) { + try { + results[name] = await check(); + if (!results[name]) allHealthy = false; + } catch { + results[name] = false; + allHealthy = false; + } + } + + return { + status: allHealthy ? "healthy" : "unhealthy", + timestamp: new Date().toISOString(), + checks: results, + uptime: process.uptime(), + }; + } +} + +// Metrics Provider +class MetricsProvider { + private counters: Map = new Map(); + private gauges: Map = new Map(); + private histograms: Map = new Map(); + private httpRequests: Map = new Map(); + private httpDurations: number[] = []; + + incrementCounter(name: string, value: number = 1) { + this.counters.set(name, (this.counters.get(name) || 0) + value); + } + + setGauge(name: string, value: number) { + this.gauges.set(name, value); + } + + recordHistogram(name: string, value: number) { + const values = this.histograms.get(name) || []; + values.push(value); + // Keep last 1000 values + if (values.length > 1000) values.shift(); + this.histograms.set(name, values); + } + + httpMetricsMiddleware() { + return (req: Request, res: Response, next: NextFunction) => { + const start = Date.now(); + + res.on("finish", () => { + const duration = Date.now() - start; + const key = `${req.method}_${res.statusCode}`; + this.httpRequests.set(key, (this.httpRequests.get(key) || 0) + 1); + this.httpDurations.push(duration); + if (this.httpDurations.length > 1000) this.httpDurations.shift(); + }); + + next(); + }; + } + + getPrometheusMetrics() { + const lines: string[] = []; + + // Process metrics + lines.push(`# HELP process_uptime_seconds Process uptime`); + lines.push(`# TYPE process_uptime_seconds gauge`); + lines.push(`process_uptime_seconds ${process.uptime()}`); + + lines.push(`# HELP process_memory_rss_bytes Resident memory size`); + lines.push(`# TYPE process_memory_rss_bytes gauge`); + lines.push(`process_memory_rss_bytes ${process.memoryUsage().rss}`); + + lines.push(`# HELP process_memory_heap_used_bytes Heap memory used`); + lines.push(`# TYPE process_memory_heap_used_bytes gauge`); + lines.push(`process_memory_heap_used_bytes ${process.memoryUsage().heapUsed}`); + + // HTTP metrics + lines.push(`# HELP http_requests_total Total HTTP requests`); + lines.push(`# TYPE http_requests_total counter`); + for (const [key, value] of this.httpRequests) { + const [method, status] = key.split("_"); + lines.push(`http_requests_total{method="${method}",status="${status}"} ${value}`); + } + + // HTTP duration + if (this.httpDurations.length > 0) { + const avg = this.httpDurations.reduce((a, b) => a + b, 0) / this.httpDurations.length; + lines.push(`# HELP http_request_duration_ms HTTP request duration`); + lines.push(`# TYPE http_request_duration_ms gauge`); + lines.push(`http_request_duration_ms_avg ${avg.toFixed(2)}`); + } + + // Custom counters + for (const [name, value] of this.counters) { + lines.push(`${name} ${value}`); + } + + // Custom gauges + for (const [name, value] of this.gauges) { + lines.push(`${name} ${value}`); + } + + return lines.join("\n"); + } +} + +// Structured Logger +class StructuredLogger { + private serviceName: string; + + constructor(serviceName: string) { + this.serviceName = serviceName; + } + + private log(level: string, message: string, data?: Record) { + console.log( + JSON.stringify({ + timestamp: new Date().toISOString(), + level, + service: this.serviceName, + message, + ...data, + }) + ); + } + + debug(message: string, data?: Record) { + this.log("debug", message, data); + } + + info(message: string, data?: Record) { + this.log("info", message, data); + } + + warn(message: string, data?: Record) { + this.log("warn", message, data); + } + + error(message: string, data?: Record) { + this.log("error", message, data); + } + + requestMiddleware() { + return (req: Request, res: Response, next: NextFunction) => { + const start = Date.now(); + const requestId = req.headers["x-request-id"] || Date.now().toString(36); + + res.setHeader("x-request-id", requestId); + + res.on("finish", () => { + this.info("HTTP Request", { + requestId, + method: req.method, + path: req.path, + status: res.statusCode, + duration: Date.now() - start, + userAgent: req.headers["user-agent"], + }); + }); + + next(); + }; + } +} + +// ============================================================================ +// Create Micro API with Observability +// ============================================================================ + +const microAPI = createMicroAPI(); + +// Create provider instances +const health = new HealthCheckProvider(); +const metrics = new MetricsProvider(); +const logger = new StructuredLogger(config.values.app.name); + +const app = microAPI.build(); + +// Add observability middleware +app.Middleware.parse(); +app.Middleware.add(metrics.httpMetricsMiddleware()); +app.Middleware.add(logger.requestMiddleware()); + +// Register health checks +health.addCheck("memory", async () => { + const used = process.memoryUsage().heapUsed / 1024 / 1024; + return used < 500; // Healthy if less than 500MB +}); + +health.addCheck("uptime", async () => { + return process.uptime() > 0; +}); + +// Simulated external service check +health.addCheck("database", async () => { + // In real app: return await db.ping(); + return true; +}); + +// ============================================================================ +// Observability Endpoints +// ============================================================================ + +// Health check (general) +app.Route.get("/health", async (req: Request, res: Response) => { + const result = await health.getHealth(); + res.status(result.status === "healthy" ? 200 : 503).json(result); +}); + +// Kubernetes readiness probe +app.Route.get("/health/ready", async (req: Request, res: Response) => { + const result = await health.getHealth(); + res.status(result.status === "healthy" ? 200 : 503).json({ + status: result.status === "healthy" ? "ready" : "not ready", + timestamp: result.timestamp, + }); +}); + +// Kubernetes liveness probe +app.Route.get("/health/live", (req: Request, res: Response) => { + res.json({ + status: "alive", + timestamp: new Date().toISOString(), + }); +}); + +// Prometheus metrics endpoint +app.Route.get("/metrics", (req: Request, res: Response) => { + res.set("Content-Type", "text/plain; version=0.0.4"); + res.send(metrics.getPrometheusMetrics()); +}); + +// ============================================================================ +// Application Endpoints +// ============================================================================ + +app.Route.get("/", (req: Request, res: Response) => { + logger.info("Root endpoint accessed"); + res.json({ + name: config.values.app.name, + version: config.values.app.version, + observability: { + health: "/health", + ready: "/health/ready", + live: "/health/live", + metrics: "/metrics", + }, + }); +}); + +// Sample endpoint that tracks metrics +app.Route.get("/api/data", (req: Request, res: Response) => { + metrics.incrementCounter("api_data_requests_total"); + logger.info("Data endpoint accessed"); + + res.json({ + data: [1, 2, 3, 4, 5], + timestamp: new Date().toISOString(), + }); +}); + +// Sample endpoint that might fail +app.Route.get("/api/random", (req: Request, res: Response) => { + metrics.incrementCounter("api_random_requests_total"); + + if (Math.random() < 0.1) { + logger.warn("Random failure triggered"); + res.status(500).json({ error: "Random failure" }); + return; + } + + res.json({ value: Math.random() }); +}); + +// ============================================================================ +// Start Server +// ============================================================================ + +const port = config.values.server.port; +app.listen(port); + +logger.info("Server started", { port }); + +console.log(` +๐Ÿš€ Observability Example running on http://localhost:${port} + +Observability Endpoints: + GET /health - Full health check + GET /health/ready - K8s readiness probe + GET /health/live - K8s liveness probe + GET /metrics - Prometheus metrics + +Application Endpoints: + GET / - API info + GET /api/data - Sample data endpoint + GET /api/random - Random endpoint (10% failure rate) + +Test with: + curl http://localhost:${port}/health + curl http://localhost:${port}/metrics + curl http://localhost:${port}/api/data + +Logs output as JSON for ELK/Loki aggregation. +`); diff --git a/micro/src/examples/serverless-lambda.example.ts b/micro/src/examples/serverless-lambda.example.ts new file mode 100644 index 0000000..4c7b6ff --- /dev/null +++ b/micro/src/examples/serverless-lambda.example.ts @@ -0,0 +1,146 @@ +/** + * AWS Lambda Serverless Example + * + * This example demonstrates how to deploy the ExpressoTS micro template + * to AWS Lambda using the serverless-http adapter. + * + * Usage: + * 1. npm install serverless-http + * 2. Build: npm run build + * 3. Deploy: serverless deploy + */ + +import { createMicroAPI } from "@expressots/adapter-express"; +import { Request, Response } from "express"; + +// ============================================================================ +// Create Micro API +// ============================================================================ + +const microAPI = createMicroAPI(); +const app = microAPI.build(); + +// Configure middleware +app.Middleware.parse(); +app.Middleware.security({ cors: true }); + +// ============================================================================ +// Routes +// ============================================================================ + +app.Route.get("/", (req: Request, res: Response) => { + res.json({ + message: "Hello from ExpressoTS on Lambda!", + requestId: (req as any).lambda?.context?.awsRequestId || "local", + timestamp: new Date().toISOString(), + }); +}); + +app.Route.get("/health", (req: Request, res: Response) => { + res.json({ + status: "healthy", + platform: "aws-lambda", + timestamp: new Date().toISOString(), + }); +}); + +app.Route.get("/users", (req: Request, res: Response) => { + res.json([ + { id: 1, name: "John Doe" }, + { id: 2, name: "Jane Smith" }, + ]); +}); + +app.Route.get("/users/:id", (req: Request, res: Response) => { + const { id } = req.params; + res.json({ id, name: `User ${id}` }); +}); + +app.Route.post("/users", (req: Request, res: Response) => { + const { name, email } = req.body; + res.status(201).json({ + id: Date.now().toString(), + name, + email, + createdAt: new Date().toISOString(), + }); +}); + +// ============================================================================ +// Lambda Handler Export +// ============================================================================ + +// For Lambda deployment, use serverless-http +let handler: any; + +const createHandler = async () => { + if (!handler) { + const serverlessHttp = await import("serverless-http"); + // Get the underlying Express app + const expressApp = (app as any).getExpressApp?.() || (microAPI as any).app; + handler = serverlessHttp.default(expressApp); + } + return handler; +}; + +// Export for Lambda +export const lambdaHandler = async (event: any, context: any) => { + const h = await createHandler(); + return h(event, context); +}; + +// For local development +if (process.env.NODE_ENV !== "production") { + const PORT = process.env.PORT || 3000; + app.listen(Number(PORT)); + console.log(` +๐Ÿš€ Lambda-ready API running locally on http://localhost:${PORT} + +Endpoints: + GET / - Hello message + GET /health - Health check + GET /users - List users + GET /users/:id - Get user by ID + POST /users - Create user + +Deploy to Lambda: + 1. npm run build + 2. serverless deploy +`); +} + +/* +serverless.yml configuration: + +service: expressots-micro-lambda +provider: + name: aws + runtime: nodejs20.x + region: us-east-1 + memorySize: 256 + timeout: 30 + environment: + NODE_ENV: production + +functions: + api: + handler: dist/src/examples/serverless-lambda.example.lambdaHandler + events: + - httpApi: + path: /{proxy+} + method: ANY + - httpApi: + path: / + method: ANY + +plugins: + - serverless-offline + +package: + patterns: + - '!node_modules/**' + - 'node_modules/@expressots/**' + - 'node_modules/express/**' + - 'node_modules/serverless-http/**' + - 'dist/**' +*/ From 868f96e4ecb379dde6a8075383335ec7e5b6c6af Mon Sep 17 00:00:00 2001 From: Richard Zampieri Date: Sun, 11 Jan 2026 01:25:28 -0800 Subject: [PATCH 15/21] chore: remove local expressots package dependencies from package.json --- micro/package.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/micro/package.json b/micro/package.json index 3e40551..965d56e 100644 --- a/micro/package.json +++ b/micro/package.json @@ -16,14 +16,10 @@ "lint": "eslint \"src/**/*.ts\" --fix" }, "dependencies": { - "@expressots/adapter-express": "file:../../adapter-express", - "@expressots/core": "file:../../expressots/expressots-core-4.0.0-beta.1.tgz", "express": "^5.2.1", "reflect-metadata": "^0.2.2" }, "devDependencies": { - "@expressots/cli": "file:../../expressots-cli", - "@expressots/shared": "file:../../shared", "@types/express": "^5.0.0", "@types/jest": "^29.5.14", "@types/node": "^22.10.2", From b24a62d54767f9bd0097a058bdd0437f69df7a18 Mon Sep 17 00:00:00 2001 From: Richard Zampieri Date: Sun, 11 Jan 2026 01:48:12 -0800 Subject: [PATCH 16/21] feat: update microservice examples and package dependencies - Added local dependencies for @expressots/adapter-express and @expressots/core in package.json. - Refactored di-container.example.ts to simplify user service implementation and logging. - Updated event-driven.example.ts to use a simple event emitter pattern and improved event handling. - Enhanced serverless-lambda.example.ts with clearer Lambda handler creation instructions and updated deployment steps. --- micro/package.json | 3 + micro/src/examples/di-container.example.ts | 198 ++++++------------ micro/src/examples/event-driven.example.ts | 45 ++-- .../src/examples/serverless-lambda.example.ts | 33 ++- 4 files changed, 119 insertions(+), 160 deletions(-) diff --git a/micro/package.json b/micro/package.json index 965d56e..fcb8b78 100644 --- a/micro/package.json +++ b/micro/package.json @@ -16,6 +16,8 @@ "lint": "eslint \"src/**/*.ts\" --fix" }, "dependencies": { + "@expressots/adapter-express": "file:../../adapter-express/expressots-adapter-express-4.0.0-beta.1.tgz", + "@expressots/core": "file:../../expressots/expressots-core-4.0.0-beta.1.tgz", "express": "^5.2.1", "reflect-metadata": "^0.2.2" }, @@ -28,6 +30,7 @@ "prettier": "^3.4.2", "supertest": "^7.0.0", "ts-jest": "^29.2.5", + "ts-node": "^10.9.2", "tsx": "^4.19.2", "typescript": "^5.7.2" } diff --git a/micro/src/examples/di-container.example.ts b/micro/src/examples/di-container.example.ts index aff2a94..28fd14c 100644 --- a/micro/src/examples/di-container.example.ts +++ b/micro/src/examples/di-container.example.ts @@ -3,6 +3,9 @@ * * This example demonstrates how to use the DI container in the * ExpressoTS micro template for service registration and resolution. + * + * Note: The micro template uses a simplified DI pattern. For full + * InversifyJS DI with decorators, use the full application template. */ import { createMicroAPI } from "@expressots/adapter-express"; @@ -26,20 +29,21 @@ const config = defineConfig({ // Service Definitions // ============================================================================ -// Repository - Data access layer -class UserRepository { - private users = [ - { id: "1", name: "John Doe", email: "john@example.com" }, - { id: "2", name: "Jane Smith", email: "jane@example.com" }, - { id: "3", name: "Bob Wilson", email: "bob@example.com" }, - ]; +// Simple in-memory user storage +const usersStore = [ + { id: "1", name: "John Doe", email: "john@example.com" }, + { id: "2", name: "Jane Smith", email: "jane@example.com" }, + { id: "3", name: "Bob Wilson", email: "bob@example.com" }, +]; +// User Service - Business logic +class UserService { findAll() { - return this.users; + return usersStore; } findById(id: string) { - return this.users.find((u) => u.id === id); + return usersStore.find((u) => u.id === id); } create(name: string, email: string) { @@ -48,122 +52,52 @@ class UserRepository { name, email, }; - this.users.push(user); + usersStore.push(user); return user; } delete(id: string) { - const index = this.users.findIndex((u) => u.id === id); + const index = usersStore.findIndex((u) => u.id === id); if (index !== -1) { - this.users.splice(index, 1); + usersStore.splice(index, 1); return true; } return false; } } -// Service - Business logic layer -class UserService { - constructor(private userRepository: UserRepository) {} - - getAllUsers() { - return this.userRepository.findAll(); - } - - getUserById(id: string) { - const user = this.userRepository.findById(id); - if (!user) { - throw new Error(`User ${id} not found`); - } - return user; - } - - createUser(name: string, email: string) { - // Validation - if (!name || name.length < 2) { - throw new Error("Name must be at least 2 characters"); - } - if (!email || !email.includes("@")) { - throw new Error("Invalid email address"); - } - return this.userRepository.create(name, email); - } - - deleteUser(id: string) { - if (!this.userRepository.delete(id)) { - throw new Error(`User ${id} not found`); - } - return { success: true }; - } -} - -// Logger service -class LoggerService { - private serviceName: string; - - constructor(serviceName: string = "app") { - this.serviceName = serviceName; - } - - info(message: string, data?: Record) { - console.log( - JSON.stringify({ - timestamp: new Date().toISOString(), - level: "info", - service: this.serviceName, - message, - ...data, - }) - ); - } - - error(message: string, error?: Error) { - console.error( - JSON.stringify({ - timestamp: new Date().toISOString(), - level: "error", - service: this.serviceName, - message, - error: error?.message, - stack: error?.stack, - }) - ); - } -} - // ============================================================================ -// Create Micro API & Register Services +// Create Micro API // ============================================================================ const microAPI = createMicroAPI(); -// Register services with different scopes - -// Singleton - Same instance for entire app lifetime -// Good for: Database connections, loggers, config -microAPI.Container.addSingleton(UserRepository); -microAPI.Container.addSingleton(LoggerService); - -// Transient - New instance every time -// Good for: Stateless services, request handlers -// Note: UserService depends on UserRepository, so we need to manually create it -// In a more complex setup, you'd use inversify's proper binding - -// Manual binding for services with dependencies -const userRepo = new UserRepository(); -const userService = new UserService(userRepo); -const logger = new LoggerService("user-service"); +// Create service instance manually for micro template +// In the full template, this would be handled by the DI container +const userService = new UserService(); // Build application const app = microAPI.build(); app.Middleware.parse(); +// Simple logger function +function log(level: string, message: string, data?: Record) { + console.log( + JSON.stringify({ + timestamp: new Date().toISOString(), + level, + message, + ...data, + }) + ); +} + // ============================================================================ -// Routes using DI +// Routes using Service // ============================================================================ app.Route.get("/", (req: Request, res: Response) => { - logger.info("Root endpoint accessed"); + log("info", "Root endpoint accessed"); res.json({ name: config.values.app.name, message: "DI Container Example", @@ -178,53 +112,55 @@ app.Route.get("/", (req: Request, res: Response) => { // GET /users - List all users app.Route.get("/users", (req: Request, res: Response) => { - logger.info("Listing all users"); - const users = userService.getAllUsers(); + log("info", "Listing all users"); + const users = userService.findAll(); res.json(users); }); // GET /users/:id - Get user by ID app.Route.get("/users/:id", (req: Request, res: Response) => { const { id } = req.params; - logger.info("Getting user by ID", { userId: id }); - - try { - const user = userService.getUserById(id); - res.json(user); - } catch (error: any) { - logger.error("User not found", error); - res.status(404).json({ error: error.message }); + log("info", "Getting user by ID", { userId: id }); + + const user = userService.findById(id); + + if (!user) { + log("warn", "User not found", { userId: id }); + return res.status(404).json({ error: "User not found" }); } + + res.json(user); }); // POST /users - Create user app.Route.post("/users", (req: Request, res: Response) => { const { name, email } = req.body; - logger.info("Creating user", { name, email }); - - try { - const user = userService.createUser(name, email); - logger.info("User created", { userId: user.id }); - res.status(201).json(user); - } catch (error: any) { - logger.error("Failed to create user", error); - res.status(400).json({ error: error.message }); + log("info", "Creating user", { name, email }); + + if (!name || !email) { + return res.status(400).json({ error: "name and email required" }); } + + const user = userService.create(name, email); + + log("info", "User created", { userId: user.id }); + res.status(201).json(user); }); // DELETE /users/:id - Delete user app.Route.delete("/users/:id", (req: Request, res: Response) => { const { id } = req.params; - logger.info("Deleting user", { userId: id }); - - try { - userService.deleteUser(id); - logger.info("User deleted", { userId: id }); - res.json({ message: `User ${id} deleted` }); - } catch (error: any) { - logger.error("Failed to delete user", error); - res.status(404).json({ error: error.message }); + log("info", "Deleting user", { userId: id }); + + const deleted = userService.delete(id); + + if (!deleted) { + log("warn", "User not found for deletion", { userId: id }); + return res.status(404).json({ error: "User not found" }); } + + log("info", "User deleted", { userId: id }); + res.json({ message: `User ${id} deleted` }); }); // Health check @@ -245,10 +181,8 @@ app.listen(port); console.log(` ๐Ÿš€ DI Container Example running on http://localhost:${port} -Services registered: - - UserRepository (Singleton) - - UserService (uses UserRepository) - - LoggerService (Singleton) +Services: + - UserService (manual instantiation) Endpoints: GET / - API info diff --git a/micro/src/examples/event-driven.example.ts b/micro/src/examples/event-driven.example.ts index 286f79e..0dba804 100644 --- a/micro/src/examples/event-driven.example.ts +++ b/micro/src/examples/event-driven.example.ts @@ -2,12 +2,15 @@ * Event-Driven Microservice Example * * This example demonstrates how to build an event-driven microservice - * using the ExpressoTS micro template with EventEmitter. + * using a simple event emitter pattern in the ExpressoTS micro template. + * + * Note: For full EventEmitter integration with DI, use the full template. */ import { createMicroAPI } from "@expressots/adapter-express"; -import { EventEmitter, defineConfig, Env, loadEnvSync } from "@expressots/core"; +import { defineConfig, Env, loadEnvSync } from "@expressots/core"; import { Request, Response } from "express"; +import { EventEmitter as NodeEventEmitter } from "events"; // Load environment loadEnvSync({ files: { development: ".env.local", production: ".env.prod" } }); @@ -22,12 +25,12 @@ const config = defineConfig({ }, }); -// Create micro API with EventEmitter -const microAPI = createMicroAPI(); -microAPI.Container.addSingleton(EventEmitter); +// Create a simple event emitter for this micro service +const events = new NodeEventEmitter(); +// Create micro API +const microAPI = createMicroAPI(); const app = microAPI.build(); -const events = microAPI.Container.get(EventEmitter); app.Middleware.parse(); @@ -36,7 +39,7 @@ app.Middleware.parse(); // ============================================================================ // Order events -events.on("order.created", async (data: { orderId: string; userId: string }) => { +events.on("order.created", (data: { orderId: string; userId: string }) => { console.log(`[Event] Order created: ${data.orderId} by user ${data.userId}`); // In a real app: // - Send confirmation email @@ -44,14 +47,14 @@ events.on("order.created", async (data: { orderId: string; userId: string }) => // - Notify warehouse }); -events.on("order.shipped", async (data: { orderId: string; trackingNumber: string }) => { +events.on("order.shipped", (data: { orderId: string; trackingNumber: string }) => { console.log(`[Event] Order shipped: ${data.orderId}, tracking: ${data.trackingNumber}`); // In a real app: // - Send shipping notification // - Update order status }); -events.on("order.cancelled", async (data: { orderId: string; reason: string }) => { +events.on("order.cancelled", (data: { orderId: string; reason: string }) => { console.log(`[Event] Order cancelled: ${data.orderId}, reason: ${data.reason}`); // In a real app: // - Process refund @@ -60,7 +63,7 @@ events.on("order.cancelled", async (data: { orderId: string; reason: string }) = }); // User events -events.on("user.registered", async (data: { userId: string; email: string }) => { +events.on("user.registered", (data: { userId: string; email: string }) => { console.log(`[Event] User registered: ${data.userId} (${data.email})`); // In a real app: // - Send welcome email @@ -81,7 +84,7 @@ app.Route.get("/", (req: Request, res: Response) => { }); // Create order - emits event -app.Route.post("/orders", async (req: Request, res: Response) => { +app.Route.post("/orders", (req: Request, res: Response) => { const { userId, items, total } = req.body; if (!userId || !items) { @@ -98,7 +101,7 @@ app.Route.post("/orders", async (req: Request, res: Response) => { }; // Emit event for async processing - await events.emit("order.created", { + events.emit("order.created", { orderId: order.id, userId: order.userId, }); @@ -107,7 +110,7 @@ app.Route.post("/orders", async (req: Request, res: Response) => { }); // Ship order - emits event -app.Route.post("/orders/:id/ship", async (req: Request, res: Response) => { +app.Route.post("/orders/:id/ship", (req: Request, res: Response) => { const { id } = req.params; const { trackingNumber } = req.body; @@ -116,7 +119,7 @@ app.Route.post("/orders/:id/ship", async (req: Request, res: Response) => { } // Emit event - await events.emit("order.shipped", { + events.emit("order.shipped", { orderId: id, trackingNumber, }); @@ -129,12 +132,12 @@ app.Route.post("/orders/:id/ship", async (req: Request, res: Response) => { }); // Cancel order - emits event -app.Route.post("/orders/:id/cancel", async (req: Request, res: Response) => { +app.Route.post("/orders/:id/cancel", (req: Request, res: Response) => { const { id } = req.params; const { reason } = req.body; // Emit event - await events.emit("order.cancelled", { + events.emit("order.cancelled", { orderId: id, reason: reason || "No reason provided", }); @@ -146,7 +149,7 @@ app.Route.post("/orders/:id/cancel", async (req: Request, res: Response) => { }); // Register user - emits event -app.Route.post("/users", async (req: Request, res: Response) => { +app.Route.post("/users", (req: Request, res: Response) => { const { email, name } = req.body; if (!email || !name) { @@ -161,7 +164,7 @@ app.Route.post("/users", async (req: Request, res: Response) => { }; // Emit event - await events.emit("user.registered", { + events.emit("user.registered", { userId: user.id, email: user.email, }); @@ -178,6 +181,12 @@ app.Route.get("/events/stats", (req: Request, res: Response) => { "order.cancelled", "user.registered", ], + listenerCounts: { + "order.created": events.listenerCount("order.created"), + "order.shipped": events.listenerCount("order.shipped"), + "order.cancelled": events.listenerCount("order.cancelled"), + "user.registered": events.listenerCount("user.registered"), + }, timestamp: new Date().toISOString(), }); }); diff --git a/micro/src/examples/serverless-lambda.example.ts b/micro/src/examples/serverless-lambda.example.ts index 4c7b6ff..673af08 100644 --- a/micro/src/examples/serverless-lambda.example.ts +++ b/micro/src/examples/serverless-lambda.example.ts @@ -31,7 +31,6 @@ app.Middleware.security({ cors: true }); app.Route.get("/", (req: Request, res: Response) => { res.json({ message: "Hello from ExpressoTS on Lambda!", - requestId: (req as any).lambda?.context?.awsRequestId || "local", timestamp: new Date().toISOString(), }); }); @@ -70,21 +69,34 @@ app.Route.post("/users", (req: Request, res: Response) => { // Lambda Handler Export // ============================================================================ +// Get the underlying Express app for serverless-http +const expressApp = (microAPI as any).app; + // For Lambda deployment, use serverless-http +// Note: Install with: npm install serverless-http let handler: any; -const createHandler = async () => { +/** + * Create Lambda handler lazily + * Install serverless-http with: npm install serverless-http + */ +async function createHandler() { if (!handler) { - const serverlessHttp = await import("serverless-http"); - // Get the underlying Express app - const expressApp = (app as any).getExpressApp?.() || (microAPI as any).app; - handler = serverlessHttp.default(expressApp); + try { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const serverlessHttp = require("serverless-http"); + handler = serverlessHttp(expressApp); + } catch { + throw new Error( + "serverless-http is not installed. Install with: npm install serverless-http" + ); + } } return handler; -}; +} // Export for Lambda -export const lambdaHandler = async (event: any, context: any) => { +export const lambdaHandler = async (event: unknown, context: unknown) => { const h = await createHandler(); return h(event, context); }; @@ -104,8 +116,9 @@ Endpoints: POST /users - Create user Deploy to Lambda: - 1. npm run build - 2. serverless deploy + 1. npm install serverless-http + 2. npm run build + 3. serverless deploy `); } From fd1f3f90bded1290f199ba333eec8049376a0827 Mon Sep 17 00:00:00 2001 From: Richard Zampieri Date: Sun, 11 Jan 2026 02:15:54 -0800 Subject: [PATCH 17/21] chore: remove deprecated files and examples from microservice template - Deleted Docker Compose and Dockerfile as they are no longer needed. - Removed example environment configuration and Jest configuration files. - Eliminated Kubernetes manifests including ConfigMap, Deployment, Ingress, and Network Policy. - Cleared out example implementations for dependency injection, event-driven architecture, and observability. - Updated package.json to reflect the removal of unnecessary dependencies and scripts. --- micro/Dockerfile | 57 --- micro/README.md | 52 +-- micro/docker-compose.yml | 173 --------- micro/env.example | 33 -- micro/jest.config.ts | 15 - micro/k8s/configmap.yml | 59 --- micro/k8s/deployment.yml | 191 ---------- micro/k8s/ingress.yml | 105 ------ micro/package.json | 14 +- micro/src/api.ts | 158 +------- micro/src/examples/di-container.example.ts | 199 ---------- micro/src/examples/event-driven.example.ts | 213 ----------- micro/src/examples/observability.example.ts | 348 ------------------ .../src/examples/serverless-lambda.example.ts | 159 -------- micro/test/api.spec.ts | 76 ---- 15 files changed, 9 insertions(+), 1843 deletions(-) delete mode 100644 micro/Dockerfile delete mode 100644 micro/docker-compose.yml delete mode 100644 micro/env.example delete mode 100644 micro/jest.config.ts delete mode 100644 micro/k8s/configmap.yml delete mode 100644 micro/k8s/deployment.yml delete mode 100644 micro/k8s/ingress.yml delete mode 100644 micro/src/examples/di-container.example.ts delete mode 100644 micro/src/examples/event-driven.example.ts delete mode 100644 micro/src/examples/observability.example.ts delete mode 100644 micro/src/examples/serverless-lambda.example.ts delete mode 100644 micro/test/api.spec.ts diff --git a/micro/Dockerfile b/micro/Dockerfile deleted file mode 100644 index 59a2094..0000000 --- a/micro/Dockerfile +++ /dev/null @@ -1,57 +0,0 @@ -# ExpressoTS Micro Template - Production Dockerfile -# Multi-stage build for optimal image size - -# ============================================================================ -# Build Stage -# ============================================================================ -FROM node:20-alpine AS builder - -# Set working directory -WORKDIR /app - -# Install dependencies first (for better caching) -COPY package*.json ./ -RUN npm ci --only=production && npm cache clean --force - -# Copy source files -COPY . . - -# Build TypeScript -RUN npm run build - -# Remove dev dependencies -RUN npm prune --production - -# ============================================================================ -# Production Stage -# ============================================================================ -FROM node:20-alpine AS production - -# Set working directory -WORKDIR /app - -# Create non-root user for security -RUN addgroup -g 1001 -S nodejs && \ - adduser -S expressots -u 1001 -G nodejs - -# Copy built files from builder -COPY --from=builder --chown=expressots:nodejs /app/node_modules ./node_modules -COPY --from=builder --chown=expressots:nodejs /app/dist ./dist -COPY --from=builder --chown=expressots:nodejs /app/package.json ./ - -# Set environment variables -ENV NODE_ENV=production -ENV PORT=3000 - -# Switch to non-root user -USER expressots - -# Expose port -EXPOSE 3000 - -# Health check for container orchestration -HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ - CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1 - -# Start the application -CMD ["node", "dist/src/api.js"] diff --git a/micro/README.md b/micro/README.md index 29fd666..74446bd 100644 --- a/micro/README.md +++ b/micro/README.md @@ -2,13 +2,6 @@ A lightweight, minimal ExpressoTS microservice template. -## Features - -- ๐Ÿš€ **Minimal Footprint** - Just the essentials for microservices -- ๐Ÿ”ง **Type-Safe Config** - Environment variables with full TypeScript support -- โšก **Fast Startup** - Optimized for serverless and containers -- ๐Ÿงช **Testing Ready** - Jest configured out of the box - ## Quick Start ```bash @@ -29,36 +22,14 @@ npm run prod ``` src/ -โ””โ”€โ”€ api.ts # Single file containing all routes and config -``` - -## Configuration - -The micro template uses inline configuration with `defineConfig`: - -```typescript -const config = defineConfig({ - app: { - name: Env.string("APP_NAME", { default: "ExpressoTS Micro" }), - }, - server: { - port: Env.number("PORT", { default: 3000 }), - }, -}); +โ””โ”€โ”€ api.ts # Single file API ``` -## API Endpoints - -| Method | Path | Description | -|--------|------|-------------| -| GET | / | Service info | -| GET | /health | Health check | - ## Adding Routes ```typescript -app.Route.get("/users", (req, res) => { - res.json({ users: [] }); +app.Route.get("/users", () => { + return { users: [] }; }); app.Route.post("/users", (req, res) => { @@ -67,23 +38,6 @@ app.Route.post("/users", (req, res) => { }); ``` -## Environment Variables - -Create a `.env.local` file for development: - -```env -APP_NAME="My Microservice" -APP_VERSION="1.0.0" -PORT=3000 -``` - -## Use Cases - -- Microservices in a distributed system -- Serverless functions (AWS Lambda, Vercel) -- Lightweight APIs -- Proof of concept / prototypes - ## Learn More - [ExpressoTS Documentation](https://expresso-ts.com) diff --git a/micro/docker-compose.yml b/micro/docker-compose.yml deleted file mode 100644 index ae979fb..0000000 --- a/micro/docker-compose.yml +++ /dev/null @@ -1,173 +0,0 @@ -# ExpressoTS Micro Template - Docker Compose -# For local development and testing - -version: "3.8" - -services: - # Main application - app: - build: - context: . - dockerfile: Dockerfile - ports: - - "3000:3000" - environment: - - NODE_ENV=production - - PORT=3000 - healthcheck: - test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/health"] - interval: 30s - timeout: 3s - retries: 3 - start_period: 5s - restart: unless-stopped - - # Development mode with hot reload - app-dev: - build: - context: . - dockerfile: Dockerfile.development - ports: - - "3000:3000" - environment: - - NODE_ENV=development - - PORT=3000 - volumes: - - ./src:/app/src:ro - - ./package.json:/app/package.json:ro - command: npm run dev - profiles: - - dev - -# Optional services for development (activate with profile) - - # Redis for caching/sessions - redis: - image: redis:7-alpine - ports: - - "6379:6379" - volumes: - - redis_data:/data - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 10s - timeout: 3s - retries: 3 - profiles: - - with-redis - - # PostgreSQL database - postgres: - image: postgres:16-alpine - ports: - - "5432:5432" - environment: - - POSTGRES_USER=expressots - - POSTGRES_PASSWORD=expressots - - POSTGRES_DB=expressots - volumes: - - postgres_data:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U expressots"] - interval: 10s - timeout: 3s - retries: 3 - profiles: - - with-postgres - - # MongoDB database - mongo: - image: mongo:7 - ports: - - "27017:27017" - environment: - - MONGO_INITDB_ROOT_USERNAME=expressots - - MONGO_INITDB_ROOT_PASSWORD=expressots - volumes: - - mongo_data:/data/db - profiles: - - with-mongo - - # RabbitMQ for message queuing - rabbitmq: - image: rabbitmq:3-management-alpine - ports: - - "5672:5672" - - "15672:15672" - environment: - - RABBITMQ_DEFAULT_USER=expressots - - RABBITMQ_DEFAULT_PASS=expressots - volumes: - - rabbitmq_data:/var/lib/rabbitmq - healthcheck: - test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"] - interval: 10s - timeout: 3s - retries: 3 - profiles: - - with-rabbitmq - - # Prometheus for metrics - prometheus: - image: prom/prometheus:latest - ports: - - "9090:9090" - volumes: - - ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro - command: - - "--config.file=/etc/prometheus/prometheus.yml" - profiles: - - with-monitoring - - # Grafana for dashboards - grafana: - image: grafana/grafana:latest - ports: - - "3001:3000" - environment: - - GF_SECURITY_ADMIN_PASSWORD=admin - volumes: - - grafana_data:/var/lib/grafana - profiles: - - with-monitoring - - # Jaeger for distributed tracing - jaeger: - image: jaegertracing/all-in-one:latest - ports: - - "16686:16686" - - "14268:14268" - environment: - - COLLECTOR_OTLP_ENABLED=true - profiles: - - with-tracing - -volumes: - redis_data: - postgres_data: - mongo_data: - rabbitmq_data: - grafana_data: - -# Usage: -# -# Production mode: -# docker-compose up app -# -# Development mode: -# docker-compose --profile dev up app-dev -# -# With Redis: -# docker-compose --profile with-redis up app redis -# -# With PostgreSQL: -# docker-compose --profile with-postgres up app postgres -# -# With Monitoring (Prometheus + Grafana): -# docker-compose --profile with-monitoring up app prometheus grafana -# -# With Tracing (Jaeger): -# docker-compose --profile with-tracing up app jaeger -# -# Full stack: -# docker-compose --profile with-redis --profile with-postgres --profile with-monitoring --profile with-tracing up diff --git a/micro/env.example b/micro/env.example deleted file mode 100644 index f984b9a..0000000 --- a/micro/env.example +++ /dev/null @@ -1,33 +0,0 @@ -# ExpressoTS Micro Template - Environment Configuration -# Copy this file to .env.local for development - -# Application -APP_NAME=\ ExpressoTS Micro\ -APP_VERSION=\1.0.0\ -NODE_ENV=\development\ - -# Server -PORT=3000 -HOST=\0.0.0.0\ - -# Logging -LOG_LEVEL=\debug\ -LOG_FORMAT=\pretty\ - -# Database (uncomment as needed) -# DATABASE_URL=\postgresql://user:password@localhost:5432/expressots\ - -# Redis (uncomment as needed) -# REDIS_URL=\redis://localhost:6379\ - -# Authentication (uncomment as needed) -# JWT_SECRET=\your-super-secret-jwt-key\ -# JWT_EXPIRES_IN=\7d\ - -# Observability (uncomment as needed) -# JAEGER_ENDPOINT=\http://localhost:14268/api/traces\ -# TRACING_ENABLED=\true\ - -# Message Queue (uncomment as needed) -# RABBITMQ_URL=\amqp://guest:guest@localhost:5672\ - diff --git a/micro/jest.config.ts b/micro/jest.config.ts deleted file mode 100644 index 1910514..0000000 --- a/micro/jest.config.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { JestConfigWithTsJest } from "ts-jest"; - -const jestConfig: JestConfigWithTsJest = { - preset: "ts-jest", - rootDir: "./", - testEnvironment: "node", - verbose: true, - automock: false, - testMatch: ["**/*.test.ts", "**/*.spec.ts"], - coverageDirectory: "./coverage", - coverageReporters: ["text", "html", "json"], - modulePathIgnorePatterns: ["/dist/"], -}; - -export default jestConfig; diff --git a/micro/k8s/configmap.yml b/micro/k8s/configmap.yml deleted file mode 100644 index 5963c09..0000000 --- a/micro/k8s/configmap.yml +++ /dev/null @@ -1,59 +0,0 @@ -# ExpressoTS Micro Template - Kubernetes ConfigMap -# Non-sensitive configuration values - -apiVersion: v1 -kind: ConfigMap -metadata: - name: expressots-micro-config - labels: - app: expressots-micro -data: - # Application settings - APP_NAME: "ExpressoTS Micro" - APP_VERSION: "1.0.0" - NODE_ENV: "production" - PORT: "3000" - - # Logging - LOG_LEVEL: "info" - LOG_FORMAT: "json" - - # Performance - NODE_OPTIONS: "--max-old-space-size=256" - - # Observability (optional - uncomment as needed) - # JAEGER_ENDPOINT: "http://jaeger-collector:14268/api/traces" - # PROMETHEUS_ENABLED: "true" - # TRACING_ENABLED: "true" - # TRACING_SAMPLE_RATE: "1.0" - - # Service Discovery (optional - uncomment as needed) - # CONSUL_URL: "http://consul:8500" - # SERVICE_DISCOVERY_ENABLED: "true" - ---- -# Secret for sensitive values -# In production, use external secret management (Vault, AWS Secrets Manager, etc.) -apiVersion: v1 -kind: Secret -metadata: - name: expressots-micro-secrets - labels: - app: expressots-micro -type: Opaque -stringData: - # Database (if using) - # DATABASE_URL: "postgresql://user:password@postgres:5432/db" - - # Redis (if using) - # REDIS_URL: "redis://redis:6379" - - # JWT (if using auth) - # JWT_SECRET: "your-super-secret-jwt-key" - - # API Keys (if using external services) - # STRIPE_SECRET_KEY: "sk_live_..." - # SENDGRID_API_KEY: "SG..." - - # Placeholder - replace with actual secrets - PLACEHOLDER: "replace-with-real-secrets" diff --git a/micro/k8s/deployment.yml b/micro/k8s/deployment.yml deleted file mode 100644 index 0f28540..0000000 --- a/micro/k8s/deployment.yml +++ /dev/null @@ -1,191 +0,0 @@ -# ExpressoTS Micro Template - Kubernetes Deployment -# Production-ready configuration with health checks, resources, and scaling - -apiVersion: apps/v1 -kind: Deployment -metadata: - name: expressots-micro - labels: - app: expressots-micro - version: v1 -spec: - replicas: 3 - selector: - matchLabels: - app: expressots-micro - strategy: - type: RollingUpdate - rollingUpdate: - maxSurge: 1 - maxUnavailable: 0 - template: - metadata: - labels: - app: expressots-micro - version: v1 - annotations: - prometheus.io/scrape: "true" - prometheus.io/port: "3000" - prometheus.io/path: "/metrics" - spec: - serviceAccountName: expressots-micro - securityContext: - runAsNonRoot: true - runAsUser: 1001 - runAsGroup: 1001 - fsGroup: 1001 - containers: - - name: app - image: expressots-micro:latest - imagePullPolicy: Always - ports: - - name: http - containerPort: 3000 - protocol: TCP - env: - - name: NODE_ENV - value: "production" - - name: PORT - value: "3000" - envFrom: - - configMapRef: - name: expressots-micro-config - optional: true - - secretRef: - name: expressots-micro-secrets - optional: true - resources: - requests: - memory: "128Mi" - cpu: "100m" - limits: - memory: "256Mi" - cpu: "500m" - livenessProbe: - httpGet: - path: /health/live - port: 3000 - initialDelaySeconds: 30 - periodSeconds: 10 - timeoutSeconds: 3 - failureThreshold: 3 - readinessProbe: - httpGet: - path: /health/ready - port: 3000 - initialDelaySeconds: 5 - periodSeconds: 5 - timeoutSeconds: 3 - failureThreshold: 3 - securityContext: - allowPrivilegeEscalation: false - readOnlyRootFilesystem: true - capabilities: - drop: - - ALL - affinity: - podAntiAffinity: - preferredDuringSchedulingIgnoredDuringExecution: - - weight: 100 - podAffinityTerm: - labelSelector: - matchExpressions: - - key: app - operator: In - values: - - expressots-micro - topologyKey: kubernetes.io/hostname - topologySpreadConstraints: - - maxSkew: 1 - topologyKey: topology.kubernetes.io/zone - whenUnsatisfiable: ScheduleAnyway - labelSelector: - matchLabels: - app: expressots-micro - ---- -# Service -apiVersion: v1 -kind: Service -metadata: - name: expressots-micro - labels: - app: expressots-micro -spec: - type: ClusterIP - ports: - - name: http - port: 80 - targetPort: 3000 - protocol: TCP - selector: - app: expressots-micro - ---- -# Service Account -apiVersion: v1 -kind: ServiceAccount -metadata: - name: expressots-micro - labels: - app: expressots-micro - ---- -# Horizontal Pod Autoscaler -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - name: expressots-micro - labels: - app: expressots-micro -spec: - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: expressots-micro - minReplicas: 3 - maxReplicas: 10 - metrics: - - type: Resource - resource: - name: cpu - target: - type: Utilization - averageUtilization: 70 - - type: Resource - resource: - name: memory - target: - type: Utilization - averageUtilization: 80 - behavior: - scaleDown: - stabilizationWindowSeconds: 300 - policies: - - type: Percent - value: 10 - periodSeconds: 60 - scaleUp: - stabilizationWindowSeconds: 0 - policies: - - type: Percent - value: 100 - periodSeconds: 15 - - type: Pods - value: 4 - periodSeconds: 15 - selectPolicy: Max - ---- -# Pod Disruption Budget -apiVersion: policy/v1 -kind: PodDisruptionBudget -metadata: - name: expressots-micro - labels: - app: expressots-micro -spec: - minAvailable: 2 - selector: - matchLabels: - app: expressots-micro diff --git a/micro/k8s/ingress.yml b/micro/k8s/ingress.yml deleted file mode 100644 index 9d9ea32..0000000 --- a/micro/k8s/ingress.yml +++ /dev/null @@ -1,105 +0,0 @@ -# ExpressoTS Micro Template - Kubernetes Ingress -# HTTPS configuration with TLS termination - -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: expressots-micro - labels: - app: expressots-micro - annotations: - # Ingress class - kubernetes.io/ingress.class: "nginx" - - # TLS/SSL with cert-manager - cert-manager.io/cluster-issuer: "letsencrypt-prod" - - # Rate limiting - nginx.ingress.kubernetes.io/rate-limit: "100" - nginx.ingress.kubernetes.io/rate-limit-window: "1m" - - # Timeouts - nginx.ingress.kubernetes.io/proxy-connect-timeout: "30" - nginx.ingress.kubernetes.io/proxy-read-timeout: "30" - nginx.ingress.kubernetes.io/proxy-send-timeout: "30" - - # Body size limit - nginx.ingress.kubernetes.io/proxy-body-size: "10m" - - # CORS (if needed) - # nginx.ingress.kubernetes.io/enable-cors: "true" - # nginx.ingress.kubernetes.io/cors-allow-origin: "*" - - # Security headers - nginx.ingress.kubernetes.io/configuration-snippet: | - add_header X-Frame-Options "SAMEORIGIN" always; - add_header X-Content-Type-Options "nosniff" always; - add_header X-XSS-Protection "1; mode=block" always; - add_header Referrer-Policy "strict-origin-when-cross-origin" always; -spec: - ingressClassName: nginx - tls: - - hosts: - - api.example.com - secretName: expressots-micro-tls - rules: - - host: api.example.com - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: expressots-micro - port: - number: 80 - ---- -# Network Policy - Restrict traffic -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - name: expressots-micro - labels: - app: expressots-micro -spec: - podSelector: - matchLabels: - app: expressots-micro - policyTypes: - - Ingress - - Egress - ingress: - # Allow traffic from ingress controller - - from: - - namespaceSelector: - matchLabels: - name: ingress-nginx - ports: - - protocol: TCP - port: 3000 - # Allow traffic from prometheus for scraping - - from: - - namespaceSelector: - matchLabels: - name: monitoring - ports: - - protocol: TCP - port: 3000 - egress: - # Allow DNS - - to: - - namespaceSelector: {} - ports: - - protocol: UDP - port: 53 - # Allow outbound HTTPS - - to: - - ipBlock: - cidr: 0.0.0.0/0 - ports: - - protocol: TCP - port: 443 - # Allow outbound to other services in same namespace - - to: - - podSelector: {} diff --git a/micro/package.json b/micro/package.json index fcb8b78..00d1a92 100644 --- a/micro/package.json +++ b/micro/package.json @@ -9,29 +9,19 @@ "build": "expressots build", "dev": "expressots dev", "prod": "expressots prod", - "test": "jest --runInBand", - "test:watch": "jest --watchAll", - "test:cov": "jest --coverage", - "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", + "format": "prettier --write \"src/**/*.ts\"", "lint": "eslint \"src/**/*.ts\" --fix" }, "dependencies": { "@expressots/adapter-express": "file:../../adapter-express/expressots-adapter-express-4.0.0-beta.1.tgz", "@expressots/core": "file:../../expressots/expressots-core-4.0.0-beta.1.tgz", - "express": "^5.2.1", + "express": "^5.0.0", "reflect-metadata": "^0.2.2" }, "devDependencies": { "@types/express": "^5.0.0", - "@types/jest": "^29.5.14", "@types/node": "^22.10.2", - "jest": "^29.7.0", - "nodemon": "^3.1.11", "prettier": "^3.4.2", - "supertest": "^7.0.0", - "ts-jest": "^29.2.5", - "ts-node": "^10.9.2", - "tsx": "^4.19.2", "typescript": "^5.7.2" } } diff --git a/micro/src/api.ts b/micro/src/api.ts index 7ad0915..8bd2669 100644 --- a/micro/src/api.ts +++ b/micro/src/api.ts @@ -1,164 +1,14 @@ import { createMicroAPI } from "@expressots/adapter-express"; -import { defineConfig, Env, loadEnvSync } from "@expressots/core"; -import { Request, Response } from "express"; - -// ============================================================================ -// Environment Configuration -// ============================================================================ - -loadEnvSync({ - files: { development: ".env.local", production: ".env.prod" }, -}); - -// ============================================================================ -// Type-Safe Configuration -// ============================================================================ - -const config = defineConfig({ - app: { - name: Env.string("APP_NAME", { default: "ExpressoTS Micro" }), - version: Env.string("APP_VERSION", { default: "1.0.0" }), - }, - server: { - port: Env.number("PORT", { default: 3000 }), - }, -}); - -// ============================================================================ -// Create Micro API -// ============================================================================ const microAPI = createMicroAPI(); - -// ============================================================================ -// Optional: Register Services in DI Container -// ============================================================================ - -// Example service registration (uncomment to use): -// -// class UserService { -// findAll() { -// return [{ id: 1, name: "John Doe" }]; -// } -// findById(id: string) { -// return { id, name: "John Doe", email: "john@example.com" }; -// } -// } -// -// microAPI.Container.addSingleton(UserService); - -// ============================================================================ -// Build Application -// ============================================================================ - const app = microAPI.build(); -// ============================================================================ -// Middleware Configuration (V4 Unified Methods) -// ============================================================================ - -// Request parsing (JSON + URL-encoded) app.Middleware.parse(); -// Security (CORS enabled) -app.Middleware.security({ cors: true }); - -// Error handler -app.Middleware.setErrorHandler({ - showStackTrace: process.env.NODE_ENV === "development", -}); - -// ============================================================================ -// Routes -// ============================================================================ - -// Root endpoint - Service info -app.Route.get("/", (req: Request, res: Response) => { - res.json({ - name: config.values.app.name, - version: config.values.app.version, - message: "Hello from ExpressoTS Micro API!", - timestamp: new Date().toISOString(), - }); -}); - -// Health check - K8s compatible -app.Route.get("/health", (req: Request, res: Response) => { - res.json({ - status: "healthy", - timestamp: new Date().toISOString(), - uptime: process.uptime(), - environment: process.env.NODE_ENV || "development", - }); -}); - -// Kubernetes readiness probe -app.Route.get("/health/ready", (req: Request, res: Response) => { - // Add custom readiness checks here - // e.g., database connection, external services - const isReady = true; - - res.status(isReady ? 200 : 503).json({ - status: isReady ? "ready" : "not ready", - timestamp: new Date().toISOString(), - }); -}); - -// Kubernetes liveness probe -app.Route.get("/health/live", (req: Request, res: Response) => { - res.json({ - status: "alive", - timestamp: new Date().toISOString(), - }); +app.Route.get("/", () => { + return "Hello from ExpressoTS Micro API!"; }); -// ============================================================================ -// Example Routes (Uncomment to use) -// ============================================================================ - -// Example: GET /users - Using DI container -// app.Route.get("/users", (req: Request, res: Response) => { -// const userService = microAPI.Container.get(UserService); -// const users = userService.findAll(); -// res.json(users); -// }); - -// Example: GET /users/:id - Route parameters -// app.Route.get("/users/:id", (req: Request, res: Response) => { -// const userService = microAPI.Container.get(UserService); -// const user = userService.findById(req.params.id); -// if (!user) { -// return res.status(404).json({ error: "User not found" }); -// } -// res.json(user); -// }); - -// Example: POST /users - With request body -// app.Route.post("/users", (req: Request, res: Response) => { -// const { name, email } = req.body; -// if (!name || !email) { -// return res.status(400).json({ error: "Name and email required" }); -// } -// const user = { id: Date.now().toString(), name, email }; -// res.status(201).json(user); -// }); - -// ============================================================================ -// Start Server -// ============================================================================ - -const port = config.values.server.port; - -app.listen(port, { - appName: config.values.app.name, - appVersion: config.values.app.version, -}); +app.listen(3001); -console.log(` -๐Ÿš€ ${config.values.app.name} v${config.values.app.version} - - Server: http://localhost:${port} - Health: http://localhost:${port}/health - Ready: http://localhost:${port}/health/ready - Live: http://localhost:${port}/health/live -`); +console.log("Server is running on port 3001"); diff --git a/micro/src/examples/di-container.example.ts b/micro/src/examples/di-container.example.ts deleted file mode 100644 index 28fd14c..0000000 --- a/micro/src/examples/di-container.example.ts +++ /dev/null @@ -1,199 +0,0 @@ -/** - * Dependency Injection Container Example - * - * This example demonstrates how to use the DI container in the - * ExpressoTS micro template for service registration and resolution. - * - * Note: The micro template uses a simplified DI pattern. For full - * InversifyJS DI with decorators, use the full application template. - */ - -import { createMicroAPI } from "@expressots/adapter-express"; -import { defineConfig, Env, loadEnvSync } from "@expressots/core"; -import { Request, Response } from "express"; - -// Load environment -loadEnvSync({ files: { development: ".env.local" } }); - -// Configuration -const config = defineConfig({ - app: { - name: Env.string("APP_NAME", { default: "DI Container Example" }), - }, - server: { - port: Env.number("PORT", { default: 3000 }), - }, -}); - -// ============================================================================ -// Service Definitions -// ============================================================================ - -// Simple in-memory user storage -const usersStore = [ - { id: "1", name: "John Doe", email: "john@example.com" }, - { id: "2", name: "Jane Smith", email: "jane@example.com" }, - { id: "3", name: "Bob Wilson", email: "bob@example.com" }, -]; - -// User Service - Business logic -class UserService { - findAll() { - return usersStore; - } - - findById(id: string) { - return usersStore.find((u) => u.id === id); - } - - create(name: string, email: string) { - const user = { - id: Date.now().toString(), - name, - email, - }; - usersStore.push(user); - return user; - } - - delete(id: string) { - const index = usersStore.findIndex((u) => u.id === id); - if (index !== -1) { - usersStore.splice(index, 1); - return true; - } - return false; - } -} - -// ============================================================================ -// Create Micro API -// ============================================================================ - -const microAPI = createMicroAPI(); - -// Create service instance manually for micro template -// In the full template, this would be handled by the DI container -const userService = new UserService(); - -// Build application -const app = microAPI.build(); -app.Middleware.parse(); - -// Simple logger function -function log(level: string, message: string, data?: Record) { - console.log( - JSON.stringify({ - timestamp: new Date().toISOString(), - level, - message, - ...data, - }) - ); -} - -// ============================================================================ -// Routes using Service -// ============================================================================ - -app.Route.get("/", (req: Request, res: Response) => { - log("info", "Root endpoint accessed"); - res.json({ - name: config.values.app.name, - message: "DI Container Example", - endpoints: [ - "GET /users - List all users", - "GET /users/:id - Get user by ID", - "POST /users - Create user", - "DELETE /users/:id - Delete user", - ], - }); -}); - -// GET /users - List all users -app.Route.get("/users", (req: Request, res: Response) => { - log("info", "Listing all users"); - const users = userService.findAll(); - res.json(users); -}); - -// GET /users/:id - Get user by ID -app.Route.get("/users/:id", (req: Request, res: Response) => { - const { id } = req.params; - log("info", "Getting user by ID", { userId: id }); - - const user = userService.findById(id); - - if (!user) { - log("warn", "User not found", { userId: id }); - return res.status(404).json({ error: "User not found" }); - } - - res.json(user); -}); - -// POST /users - Create user -app.Route.post("/users", (req: Request, res: Response) => { - const { name, email } = req.body; - log("info", "Creating user", { name, email }); - - if (!name || !email) { - return res.status(400).json({ error: "name and email required" }); - } - - const user = userService.create(name, email); - - log("info", "User created", { userId: user.id }); - res.status(201).json(user); -}); - -// DELETE /users/:id - Delete user -app.Route.delete("/users/:id", (req: Request, res: Response) => { - const { id } = req.params; - log("info", "Deleting user", { userId: id }); - - const deleted = userService.delete(id); - - if (!deleted) { - log("warn", "User not found for deletion", { userId: id }); - return res.status(404).json({ error: "User not found" }); - } - - log("info", "User deleted", { userId: id }); - res.json({ message: `User ${id} deleted` }); -}); - -// Health check -app.Route.get("/health", (req: Request, res: Response) => { - res.json({ - status: "healthy", - timestamp: new Date().toISOString(), - }); -}); - -// ============================================================================ -// Start Server -// ============================================================================ - -const port = config.values.server.port; -app.listen(port); - -console.log(` -๐Ÿš€ DI Container Example running on http://localhost:${port} - -Services: - - UserService (manual instantiation) - -Endpoints: - GET / - API info - GET /users - List all users - GET /users/:id - Get user by ID - POST /users - Create user (body: { name, email }) - DELETE /users/:id - Delete user - GET /health - Health check - -Try: - curl http://localhost:${port}/users - curl http://localhost:${port}/users/1 - curl -X POST http://localhost:${port}/users -H "Content-Type: application/json" -d '{"name":"Alice","email":"alice@example.com"}' -`); diff --git a/micro/src/examples/event-driven.example.ts b/micro/src/examples/event-driven.example.ts deleted file mode 100644 index 0dba804..0000000 --- a/micro/src/examples/event-driven.example.ts +++ /dev/null @@ -1,213 +0,0 @@ -/** - * Event-Driven Microservice Example - * - * This example demonstrates how to build an event-driven microservice - * using a simple event emitter pattern in the ExpressoTS micro template. - * - * Note: For full EventEmitter integration with DI, use the full template. - */ - -import { createMicroAPI } from "@expressots/adapter-express"; -import { defineConfig, Env, loadEnvSync } from "@expressots/core"; -import { Request, Response } from "express"; -import { EventEmitter as NodeEventEmitter } from "events"; - -// Load environment -loadEnvSync({ files: { development: ".env.local", production: ".env.prod" } }); - -// Configuration -const config = defineConfig({ - app: { - name: Env.string("APP_NAME", { default: "Event-Driven Micro" }), - }, - server: { - port: Env.number("PORT", { default: 3000 }), - }, -}); - -// Create a simple event emitter for this micro service -const events = new NodeEventEmitter(); - -// Create micro API -const microAPI = createMicroAPI(); -const app = microAPI.build(); - -app.Middleware.parse(); - -// ============================================================================ -// Event Handlers -// ============================================================================ - -// Order events -events.on("order.created", (data: { orderId: string; userId: string }) => { - console.log(`[Event] Order created: ${data.orderId} by user ${data.userId}`); - // In a real app: - // - Send confirmation email - // - Update inventory - // - Notify warehouse -}); - -events.on("order.shipped", (data: { orderId: string; trackingNumber: string }) => { - console.log(`[Event] Order shipped: ${data.orderId}, tracking: ${data.trackingNumber}`); - // In a real app: - // - Send shipping notification - // - Update order status -}); - -events.on("order.cancelled", (data: { orderId: string; reason: string }) => { - console.log(`[Event] Order cancelled: ${data.orderId}, reason: ${data.reason}`); - // In a real app: - // - Process refund - // - Restore inventory - // - Send cancellation email -}); - -// User events -events.on("user.registered", (data: { userId: string; email: string }) => { - console.log(`[Event] User registered: ${data.userId} (${data.email})`); - // In a real app: - // - Send welcome email - // - Create default settings - // - Track analytics -}); - -// ============================================================================ -// Routes -// ============================================================================ - -app.Route.get("/", (req: Request, res: Response) => { - res.json({ - name: config.values.app.name, - message: "Event-Driven Microservice Example", - events: ["order.created", "order.shipped", "order.cancelled", "user.registered"], - }); -}); - -// Create order - emits event -app.Route.post("/orders", (req: Request, res: Response) => { - const { userId, items, total } = req.body; - - if (!userId || !items) { - return res.status(400).json({ error: "userId and items required" }); - } - - const order = { - id: `order_${Date.now()}`, - userId, - items, - total: total || 0, - status: "created", - createdAt: new Date().toISOString(), - }; - - // Emit event for async processing - events.emit("order.created", { - orderId: order.id, - userId: order.userId, - }); - - res.status(201).json(order); -}); - -// Ship order - emits event -app.Route.post("/orders/:id/ship", (req: Request, res: Response) => { - const { id } = req.params; - const { trackingNumber } = req.body; - - if (!trackingNumber) { - return res.status(400).json({ error: "trackingNumber required" }); - } - - // Emit event - events.emit("order.shipped", { - orderId: id, - trackingNumber, - }); - - res.json({ - orderId: id, - status: "shipped", - trackingNumber, - }); -}); - -// Cancel order - emits event -app.Route.post("/orders/:id/cancel", (req: Request, res: Response) => { - const { id } = req.params; - const { reason } = req.body; - - // Emit event - events.emit("order.cancelled", { - orderId: id, - reason: reason || "No reason provided", - }); - - res.json({ - orderId: id, - status: "cancelled", - }); -}); - -// Register user - emits event -app.Route.post("/users", (req: Request, res: Response) => { - const { email, name } = req.body; - - if (!email || !name) { - return res.status(400).json({ error: "email and name required" }); - } - - const user = { - id: `user_${Date.now()}`, - email, - name, - createdAt: new Date().toISOString(), - }; - - // Emit event - events.emit("user.registered", { - userId: user.id, - email: user.email, - }); - - res.status(201).json(user); -}); - -// Event statistics -app.Route.get("/events/stats", (req: Request, res: Response) => { - res.json({ - registeredEvents: [ - "order.created", - "order.shipped", - "order.cancelled", - "user.registered", - ], - listenerCounts: { - "order.created": events.listenerCount("order.created"), - "order.shipped": events.listenerCount("order.shipped"), - "order.cancelled": events.listenerCount("order.cancelled"), - "user.registered": events.listenerCount("user.registered"), - }, - timestamp: new Date().toISOString(), - }); -}); - -// Health check -app.Route.get("/health", (req: Request, res: Response) => { - res.json({ status: "healthy", timestamp: new Date().toISOString() }); -}); - -// Start server -const port = config.values.server.port; -app.listen(port); - -console.log(` -๐Ÿš€ Event-Driven Microservice running on http://localhost:${port} - -Endpoints: - POST /orders - Create order (emits order.created) - POST /orders/:id/ship - Ship order (emits order.shipped) - POST /orders/:id/cancel - Cancel order (emits order.cancelled) - POST /users - Register user (emits user.registered) - GET /events/stats - Event statistics - GET /health - Health check -`); diff --git a/micro/src/examples/observability.example.ts b/micro/src/examples/observability.example.ts deleted file mode 100644 index c470b59..0000000 --- a/micro/src/examples/observability.example.ts +++ /dev/null @@ -1,348 +0,0 @@ -/** - * Observability Example - * - * This example demonstrates how to implement production-ready - * observability in the ExpressoTS micro template including: - * - Health checks (K8s ready) - * - Prometheus metrics - * - Structured logging - * - Request tracing - */ - -import { createMicroAPI } from "@expressots/adapter-express"; -import { defineConfig, Env, loadEnvSync } from "@expressots/core"; -import { Request, Response, NextFunction } from "express"; - -// Load environment -loadEnvSync({ files: { development: ".env.local" } }); - -// Configuration -const config = defineConfig({ - app: { - name: Env.string("APP_NAME", { default: "Observability Example" }), - version: Env.string("APP_VERSION", { default: "1.0.0" }), - }, - server: { - port: Env.number("PORT", { default: 3000 }), - }, -}); - -// ============================================================================ -// Observability Providers (Simplified versions - use @expressots/micro-providers in production) -// ============================================================================ - -// Health Check Provider -class HealthCheckProvider { - private checks: Map Promise> = new Map(); - - addCheck(name: string, check: () => Promise) { - this.checks.set(name, check); - } - - async getHealth() { - const results: Record = {}; - let allHealthy = true; - - for (const [name, check] of this.checks) { - try { - results[name] = await check(); - if (!results[name]) allHealthy = false; - } catch { - results[name] = false; - allHealthy = false; - } - } - - return { - status: allHealthy ? "healthy" : "unhealthy", - timestamp: new Date().toISOString(), - checks: results, - uptime: process.uptime(), - }; - } -} - -// Metrics Provider -class MetricsProvider { - private counters: Map = new Map(); - private gauges: Map = new Map(); - private histograms: Map = new Map(); - private httpRequests: Map = new Map(); - private httpDurations: number[] = []; - - incrementCounter(name: string, value: number = 1) { - this.counters.set(name, (this.counters.get(name) || 0) + value); - } - - setGauge(name: string, value: number) { - this.gauges.set(name, value); - } - - recordHistogram(name: string, value: number) { - const values = this.histograms.get(name) || []; - values.push(value); - // Keep last 1000 values - if (values.length > 1000) values.shift(); - this.histograms.set(name, values); - } - - httpMetricsMiddleware() { - return (req: Request, res: Response, next: NextFunction) => { - const start = Date.now(); - - res.on("finish", () => { - const duration = Date.now() - start; - const key = `${req.method}_${res.statusCode}`; - this.httpRequests.set(key, (this.httpRequests.get(key) || 0) + 1); - this.httpDurations.push(duration); - if (this.httpDurations.length > 1000) this.httpDurations.shift(); - }); - - next(); - }; - } - - getPrometheusMetrics() { - const lines: string[] = []; - - // Process metrics - lines.push(`# HELP process_uptime_seconds Process uptime`); - lines.push(`# TYPE process_uptime_seconds gauge`); - lines.push(`process_uptime_seconds ${process.uptime()}`); - - lines.push(`# HELP process_memory_rss_bytes Resident memory size`); - lines.push(`# TYPE process_memory_rss_bytes gauge`); - lines.push(`process_memory_rss_bytes ${process.memoryUsage().rss}`); - - lines.push(`# HELP process_memory_heap_used_bytes Heap memory used`); - lines.push(`# TYPE process_memory_heap_used_bytes gauge`); - lines.push(`process_memory_heap_used_bytes ${process.memoryUsage().heapUsed}`); - - // HTTP metrics - lines.push(`# HELP http_requests_total Total HTTP requests`); - lines.push(`# TYPE http_requests_total counter`); - for (const [key, value] of this.httpRequests) { - const [method, status] = key.split("_"); - lines.push(`http_requests_total{method="${method}",status="${status}"} ${value}`); - } - - // HTTP duration - if (this.httpDurations.length > 0) { - const avg = this.httpDurations.reduce((a, b) => a + b, 0) / this.httpDurations.length; - lines.push(`# HELP http_request_duration_ms HTTP request duration`); - lines.push(`# TYPE http_request_duration_ms gauge`); - lines.push(`http_request_duration_ms_avg ${avg.toFixed(2)}`); - } - - // Custom counters - for (const [name, value] of this.counters) { - lines.push(`${name} ${value}`); - } - - // Custom gauges - for (const [name, value] of this.gauges) { - lines.push(`${name} ${value}`); - } - - return lines.join("\n"); - } -} - -// Structured Logger -class StructuredLogger { - private serviceName: string; - - constructor(serviceName: string) { - this.serviceName = serviceName; - } - - private log(level: string, message: string, data?: Record) { - console.log( - JSON.stringify({ - timestamp: new Date().toISOString(), - level, - service: this.serviceName, - message, - ...data, - }) - ); - } - - debug(message: string, data?: Record) { - this.log("debug", message, data); - } - - info(message: string, data?: Record) { - this.log("info", message, data); - } - - warn(message: string, data?: Record) { - this.log("warn", message, data); - } - - error(message: string, data?: Record) { - this.log("error", message, data); - } - - requestMiddleware() { - return (req: Request, res: Response, next: NextFunction) => { - const start = Date.now(); - const requestId = req.headers["x-request-id"] || Date.now().toString(36); - - res.setHeader("x-request-id", requestId); - - res.on("finish", () => { - this.info("HTTP Request", { - requestId, - method: req.method, - path: req.path, - status: res.statusCode, - duration: Date.now() - start, - userAgent: req.headers["user-agent"], - }); - }); - - next(); - }; - } -} - -// ============================================================================ -// Create Micro API with Observability -// ============================================================================ - -const microAPI = createMicroAPI(); - -// Create provider instances -const health = new HealthCheckProvider(); -const metrics = new MetricsProvider(); -const logger = new StructuredLogger(config.values.app.name); - -const app = microAPI.build(); - -// Add observability middleware -app.Middleware.parse(); -app.Middleware.add(metrics.httpMetricsMiddleware()); -app.Middleware.add(logger.requestMiddleware()); - -// Register health checks -health.addCheck("memory", async () => { - const used = process.memoryUsage().heapUsed / 1024 / 1024; - return used < 500; // Healthy if less than 500MB -}); - -health.addCheck("uptime", async () => { - return process.uptime() > 0; -}); - -// Simulated external service check -health.addCheck("database", async () => { - // In real app: return await db.ping(); - return true; -}); - -// ============================================================================ -// Observability Endpoints -// ============================================================================ - -// Health check (general) -app.Route.get("/health", async (req: Request, res: Response) => { - const result = await health.getHealth(); - res.status(result.status === "healthy" ? 200 : 503).json(result); -}); - -// Kubernetes readiness probe -app.Route.get("/health/ready", async (req: Request, res: Response) => { - const result = await health.getHealth(); - res.status(result.status === "healthy" ? 200 : 503).json({ - status: result.status === "healthy" ? "ready" : "not ready", - timestamp: result.timestamp, - }); -}); - -// Kubernetes liveness probe -app.Route.get("/health/live", (req: Request, res: Response) => { - res.json({ - status: "alive", - timestamp: new Date().toISOString(), - }); -}); - -// Prometheus metrics endpoint -app.Route.get("/metrics", (req: Request, res: Response) => { - res.set("Content-Type", "text/plain; version=0.0.4"); - res.send(metrics.getPrometheusMetrics()); -}); - -// ============================================================================ -// Application Endpoints -// ============================================================================ - -app.Route.get("/", (req: Request, res: Response) => { - logger.info("Root endpoint accessed"); - res.json({ - name: config.values.app.name, - version: config.values.app.version, - observability: { - health: "/health", - ready: "/health/ready", - live: "/health/live", - metrics: "/metrics", - }, - }); -}); - -// Sample endpoint that tracks metrics -app.Route.get("/api/data", (req: Request, res: Response) => { - metrics.incrementCounter("api_data_requests_total"); - logger.info("Data endpoint accessed"); - - res.json({ - data: [1, 2, 3, 4, 5], - timestamp: new Date().toISOString(), - }); -}); - -// Sample endpoint that might fail -app.Route.get("/api/random", (req: Request, res: Response) => { - metrics.incrementCounter("api_random_requests_total"); - - if (Math.random() < 0.1) { - logger.warn("Random failure triggered"); - res.status(500).json({ error: "Random failure" }); - return; - } - - res.json({ value: Math.random() }); -}); - -// ============================================================================ -// Start Server -// ============================================================================ - -const port = config.values.server.port; -app.listen(port); - -logger.info("Server started", { port }); - -console.log(` -๐Ÿš€ Observability Example running on http://localhost:${port} - -Observability Endpoints: - GET /health - Full health check - GET /health/ready - K8s readiness probe - GET /health/live - K8s liveness probe - GET /metrics - Prometheus metrics - -Application Endpoints: - GET / - API info - GET /api/data - Sample data endpoint - GET /api/random - Random endpoint (10% failure rate) - -Test with: - curl http://localhost:${port}/health - curl http://localhost:${port}/metrics - curl http://localhost:${port}/api/data - -Logs output as JSON for ELK/Loki aggregation. -`); diff --git a/micro/src/examples/serverless-lambda.example.ts b/micro/src/examples/serverless-lambda.example.ts deleted file mode 100644 index 673af08..0000000 --- a/micro/src/examples/serverless-lambda.example.ts +++ /dev/null @@ -1,159 +0,0 @@ -/** - * AWS Lambda Serverless Example - * - * This example demonstrates how to deploy the ExpressoTS micro template - * to AWS Lambda using the serverless-http adapter. - * - * Usage: - * 1. npm install serverless-http - * 2. Build: npm run build - * 3. Deploy: serverless deploy - */ - -import { createMicroAPI } from "@expressots/adapter-express"; -import { Request, Response } from "express"; - -// ============================================================================ -// Create Micro API -// ============================================================================ - -const microAPI = createMicroAPI(); -const app = microAPI.build(); - -// Configure middleware -app.Middleware.parse(); -app.Middleware.security({ cors: true }); - -// ============================================================================ -// Routes -// ============================================================================ - -app.Route.get("/", (req: Request, res: Response) => { - res.json({ - message: "Hello from ExpressoTS on Lambda!", - timestamp: new Date().toISOString(), - }); -}); - -app.Route.get("/health", (req: Request, res: Response) => { - res.json({ - status: "healthy", - platform: "aws-lambda", - timestamp: new Date().toISOString(), - }); -}); - -app.Route.get("/users", (req: Request, res: Response) => { - res.json([ - { id: 1, name: "John Doe" }, - { id: 2, name: "Jane Smith" }, - ]); -}); - -app.Route.get("/users/:id", (req: Request, res: Response) => { - const { id } = req.params; - res.json({ id, name: `User ${id}` }); -}); - -app.Route.post("/users", (req: Request, res: Response) => { - const { name, email } = req.body; - res.status(201).json({ - id: Date.now().toString(), - name, - email, - createdAt: new Date().toISOString(), - }); -}); - -// ============================================================================ -// Lambda Handler Export -// ============================================================================ - -// Get the underlying Express app for serverless-http -const expressApp = (microAPI as any).app; - -// For Lambda deployment, use serverless-http -// Note: Install with: npm install serverless-http -let handler: any; - -/** - * Create Lambda handler lazily - * Install serverless-http with: npm install serverless-http - */ -async function createHandler() { - if (!handler) { - try { - // eslint-disable-next-line @typescript-eslint/no-require-imports - const serverlessHttp = require("serverless-http"); - handler = serverlessHttp(expressApp); - } catch { - throw new Error( - "serverless-http is not installed. Install with: npm install serverless-http" - ); - } - } - return handler; -} - -// Export for Lambda -export const lambdaHandler = async (event: unknown, context: unknown) => { - const h = await createHandler(); - return h(event, context); -}; - -// For local development -if (process.env.NODE_ENV !== "production") { - const PORT = process.env.PORT || 3000; - app.listen(Number(PORT)); - console.log(` -๐Ÿš€ Lambda-ready API running locally on http://localhost:${PORT} - -Endpoints: - GET / - Hello message - GET /health - Health check - GET /users - List users - GET /users/:id - Get user by ID - POST /users - Create user - -Deploy to Lambda: - 1. npm install serverless-http - 2. npm run build - 3. serverless deploy -`); -} - -/* -serverless.yml configuration: - -service: expressots-micro-lambda -provider: - name: aws - runtime: nodejs20.x - region: us-east-1 - memorySize: 256 - timeout: 30 - environment: - NODE_ENV: production - -functions: - api: - handler: dist/src/examples/serverless-lambda.example.lambdaHandler - events: - - httpApi: - path: /{proxy+} - method: ANY - - httpApi: - path: / - method: ANY - -plugins: - - serverless-offline - -package: - patterns: - - '!node_modules/**' - - 'node_modules/@expressots/**' - - 'node_modules/express/**' - - 'node_modules/serverless-http/**' - - 'dist/**' -*/ diff --git a/micro/test/api.spec.ts b/micro/test/api.spec.ts deleted file mode 100644 index 2b5897c..0000000 --- a/micro/test/api.spec.ts +++ /dev/null @@ -1,76 +0,0 @@ -import "reflect-metadata"; -import { createMicroAPI } from "@expressots/adapter-express"; -import { Server } from "http"; -import { createFluentRequest } from "@expressots/core"; - -describe("MicroAPI", () => { - let httpServer: Server; - let baseUrl: string; - - beforeAll(async () => { - const microAPI = createMicroAPI(); - const app = microAPI.build(); - - // Add routes for testing - app.Route.get("/", (req, res) => { - res.json({ - message: "Hello from ExpressoTS Micro!", - version: "4.0.0", - }); - }); - - app.Route.get("/health", (req, res) => { - res.json({ - status: "healthy", - timestamp: new Date().toISOString(), - }); - }); - - // Listen on random port - await app.listen(0); - httpServer = microAPI.getHttpServer(); - const address = httpServer.address() as { port: number }; - baseUrl = `http://localhost:${address.port}`; - }); - - afterAll(() => { - httpServer?.close(); - }); - - describe("GET /", () => { - it("should return welcome message", async () => { - const request = createFluentRequest(baseUrl); - const response = await request - .get("/") - .expectStatus(200) - .execute(); - - expect(response.body).toHaveProperty("message"); - expect(response.body.message).toBe("Hello from ExpressoTS Micro!"); - }); - }); - - describe("GET /health", () => { - it("should return health status", async () => { - const request = createFluentRequest(baseUrl); - const response = await request - .get("/health") - .expectStatus(200) - .execute(); - - expect(response.body.status).toBe("healthy"); - expect(response.body).toHaveProperty("timestamp"); - }); - }); - - describe("Performance", () => { - it("should respond quickly", async () => { - const request = createFluentRequest(baseUrl); - await request - .get("/") - .expectStatus(200) - .expectTime({ lessThan: 50 }) - .execute(); - }); - }); -}); From e61267fa1b3d0377c042438a43d98400a97d3c47 Mon Sep 17 00:00:00 2001 From: Richard Zampieri Date: Sun, 11 Jan 2026 04:24:29 -0800 Subject: [PATCH 18/21] fix: update adapter-express import and simplify API initialization - Changed the import from createMicroAPI to micro for a more streamlined API setup. - Updated the package.json to reference the correct path for @expressots/adapter-express. - Simplified the API initialization by removing unnecessary middleware parsing and directly defining the route handler. --- micro/package.json | 2 +- micro/src/api.ts | 11 +++-------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/micro/package.json b/micro/package.json index 00d1a92..00cdd74 100644 --- a/micro/package.json +++ b/micro/package.json @@ -13,7 +13,7 @@ "lint": "eslint \"src/**/*.ts\" --fix" }, "dependencies": { - "@expressots/adapter-express": "file:../../adapter-express/expressots-adapter-express-4.0.0-beta.1.tgz", + "@expressots/adapter-express": "file:../../adapter-express/lib/expressots-adapter-express-4.0.0-beta.1.tgz", "@expressots/core": "file:../../expressots/expressots-core-4.0.0-beta.1.tgz", "express": "^5.0.0", "reflect-metadata": "^0.2.2" diff --git a/micro/src/api.ts b/micro/src/api.ts index 8bd2669..f7b1931 100644 --- a/micro/src/api.ts +++ b/micro/src/api.ts @@ -1,14 +1,9 @@ -import { createMicroAPI } from "@expressots/adapter-express"; +import { micro } from "@expressots/adapter-express"; -const microAPI = createMicroAPI(); -const app = microAPI.build(); +const app = micro(); -app.Middleware.parse(); - -app.Route.get("/", () => { +app.get("/", () => { return "Hello from ExpressoTS Micro API!"; }); app.listen(3001); - -console.log("Server is running on port 3001"); From 08a1a6032973ab3e430a0031c7d26390ec408fcd Mon Sep 17 00:00:00 2001 From: Richard Zampieri Date: Tue, 13 Jan 2026 00:44:27 -0800 Subject: [PATCH 19/21] feat: enhance ExpressoTS Micro with advanced features and examples - Updated package.json to improve project description and add relevant keywords. - Introduced new npm scripts for example execution, showcasing advanced features like Circuit Breaker, Service Discovery, and Service Client. - Expanded README.md to reflect new features and provide detailed usage instructions. - Added example implementations for Circuit Breaker, Service Discovery, Service Client, Full DI API, and Serverless deployment. - Included comprehensive examples and documentation to facilitate understanding of advanced capabilities. --- micro/README.md | 197 +++++++++++++++++++- micro/examples/README.md | 133 +++++++++++++ micro/examples/circuit-breaker.example.ts | 86 +++++++++ micro/examples/full-di-api.example.ts | 139 ++++++++++++++ micro/examples/serverless-lambda.example.ts | 115 ++++++++++++ micro/examples/service-client.example.ts | 175 +++++++++++++++++ micro/examples/service-discovery.example.ts | 140 ++++++++++++++ micro/package.json | 30 ++- 8 files changed, 1007 insertions(+), 8 deletions(-) create mode 100644 micro/examples/README.md create mode 100644 micro/examples/circuit-breaker.example.ts create mode 100644 micro/examples/full-di-api.example.ts create mode 100644 micro/examples/serverless-lambda.example.ts create mode 100644 micro/examples/service-client.example.ts create mode 100644 micro/examples/service-discovery.example.ts diff --git a/micro/README.md b/micro/README.md index 74446bd..b36e0af 100644 --- a/micro/README.md +++ b/micro/README.md @@ -1,6 +1,6 @@ # ExpressoTS Micro -A lightweight, minimal ExpressoTS microservice template. +A lightweight, minimal ExpressoTS microservice template with powerful enterprise features. ## Quick Start @@ -23,19 +23,206 @@ npm run prod ``` src/ โ””โ”€โ”€ api.ts # Single file API + +examples/ # Advanced feature examples +โ”œโ”€โ”€ circuit-breaker.example.ts +โ”œโ”€โ”€ service-discovery.example.ts +โ”œโ”€โ”€ service-client.example.ts +โ”œโ”€โ”€ serverless-lambda.example.ts +โ””โ”€โ”€ full-di-api.example.ts ``` ## Adding Routes ```typescript -app.Route.get("/users", () => { +import { micro } from "@expressots/adapter-express"; + +const app = micro(); + +// Simple GET route - return value is auto-sent as JSON +app.get("/users", () => { return { users: [] }; }); -app.Route.post("/users", (req, res) => { +// POST route with request body +app.post("/users", (req) => { const user = req.body; - res.status(201).json(user); + return user; +}); + +// Route with parameters +app.get("/users/:id", (req) => { + return { id: req.params.id }; +}); + +// Route with query parameters +app.get("/search", (req) => { + return { query: req.query.q }; +}); + +// Use res directly for custom responses +app.post("/custom", (req, res) => { + res.status(201).json({ created: true }); +}); + +app.listen(3000); +``` + +## Middleware Support + +```typescript +// Global middleware +app.use((req, res, next) => { + console.log(`${req.method} ${req.path}`); + next(); +}); + +// Path-specific middleware +app.use("/api", authMiddleware); + +// Route-specific middleware (before handler) +const validate = (req, res, next) => { + if (!req.body.name) { + return res.status(400).json({ error: "Name required" }); + } + next(); +}; + +app.post("/users", validate, (req) => { + return { created: true }; +}); +``` + +## Error Handling + +```typescript +app.setErrorHandler((err, req, res, next) => { + console.error(err); + res.status(500).json({ error: err.message }); +}); +``` + +## Configuration + +```typescript +const app = micro({ + autoParseJson: true, // Enable JSON body parsing (default: true) + globalPrefix: "/api", // Add prefix to all routes + showBanner: true, // Show startup banner (default: true) +}); +``` + +## Advanced Features + +ExpressoTS Micro includes powerful enterprise features for building production-ready microservices: + +### Circuit Breaker + +Protect your services from cascading failures: + +```typescript +import { CircuitBreaker } from "@expressots/adapter-express"; + +const breaker = new CircuitBreaker({ + failureThreshold: 5, + successThreshold: 2, + timeout: 60000, +}); + +app.get("/external-api", async () => { + return await breaker.execute(async () => { + const response = await fetch("https://api.example.com/data"); + return response.json(); + }); +}); +``` + +### Service Discovery + +Register and discover service instances with load balancing: + +```typescript +import { ServiceDiscovery } from "@expressots/adapter-express"; + +const discovery = new ServiceDiscovery({ type: "static" }); + +discovery.registerService({ + id: "user-service-1", + name: "user-service", + host: "localhost", + port: 3001, + health: "healthy", + lastCheck: new Date(), +}); + +// Get a healthy instance (round-robin load balancing) +const instance = discovery.getService("user-service"); +``` + +### Service Client + +HTTP client with retry logic and circuit breaker integration: + +```typescript +import { ServiceClient } from "@expressots/adapter-express"; + +const client = new ServiceClient({ + name: "user-service", + baseUrl: "http://localhost:3001", + timeout: 5000, + retries: 3, + circuitBreaker: { + failureThreshold: 5, + timeout: 60000, + }, }); + +app.get("/users", async () => { + return await client.get("/api/users"); +}); +``` + +### Serverless Deployment + +Deploy to AWS Lambda, Vercel, or Cloudflare Workers. See `examples/serverless-lambda.example.ts`. + +## Upgrading to Full DI + +When you need dependency injection and more advanced features, upgrade to `createMicroAPI()`: + +```typescript +import { createMicroAPI } from "@expressots/adapter-express"; + +const microAPI = createMicroAPI(); +microAPI.setGlobalRoutePrefix("/api/v1"); + +const app = microAPI.build(); +app.Middleware.parse(); + +app.Route.get("/users", async (req, res) => { + res.json([]); +}); + +await app.listen(3000); +``` + +See `examples/full-di-api.example.ts` for a complete example. + +## Examples + +Check the `examples/` folder for complete working examples of: + +- **Circuit Breaker** - Fault tolerance pattern +- **Service Discovery** - Service registration and load balancing +- **Service Client** - HTTP client with retries +- **Serverless** - AWS Lambda deployment +- **Full DI API** - Upgrade path with dependency injection + +Run examples with: + +```bash +npm run example:circuit-breaker +npm run example:service-discovery ``` ## Learn More @@ -43,3 +230,5 @@ app.Route.post("/users", (req, res) => { - [ExpressoTS Documentation](https://expresso-ts.com) - [GitHub Repository](https://github.com/expressots) - [Discord Community](https://discord.gg/PyPJfGK) +- [Advanced Features Guide](./ADVANCED.md) +- [Upgrading Guide](./UPGRADING.md) diff --git a/micro/examples/README.md b/micro/examples/README.md new file mode 100644 index 0000000..0b486fd --- /dev/null +++ b/micro/examples/README.md @@ -0,0 +1,133 @@ +# ExpressoTS Micro Examples + +This folder contains examples demonstrating the advanced features available in ExpressoTS Micro. + +## Running Examples + +Each example can be run independently using the provided npm scripts: + +```bash +# Circuit Breaker - Fault tolerance pattern +npm run example:circuit-breaker + +# Service Discovery - Service registration and load balancing +npm run example:service-discovery + +# Service Client - HTTP client with retries +npm run example:service-client + +# Full DI API - Upgrade path with dependency injection +npm run example:full-di-api +``` + +For the serverless example, see the deployment instructions in the file. + +## Examples Overview + +### 1. Circuit Breaker (`circuit-breaker.example.ts`) + +Demonstrates the Circuit Breaker pattern for protecting your service from cascading failures: + +- **CLOSED**: Normal operation, requests pass through +- **OPEN**: Requests fail immediately (service unavailable) +- **HALF_OPEN**: Testing if service has recovered + +Key features: +- Configurable failure and success thresholds +- Automatic recovery after timeout +- Manual reset and open controls +- Statistics and monitoring + +### 2. Service Discovery (`service-discovery.example.ts`) + +Shows how to implement service discovery for microservices: + +- Static service registration +- Round-robin load balancing +- Health status tracking +- Dynamic service registration/deregistration + +Use cases: +- Microservice architecture +- Multiple service instances +- Blue/green deployments + +### 3. Service Client (`service-client.example.ts`) + +Demonstrates HTTP client for service-to-service communication: + +- Automatic retries with exponential backoff +- Request timeouts +- Circuit breaker integration +- Custom headers per request +- Query parameter handling + +Use cases: +- API Gateway pattern +- Service composition +- External API calls + +### 4. Serverless Lambda (`serverless-lambda.example.ts`) + +Shows how to deploy to AWS Lambda: + +- Same code works locally and on Lambda +- AWS SAM template example +- Environment-aware configuration +- Binary content handling + +Deployment steps included in the file. + +### 5. Full DI API (`full-di-api.example.ts`) + +Upgrade path from simple `micro()` to full `createMicroAPI()`: + +- Dependency injection container +- Provider registration (singleton, transient) +- Middleware pipeline +- Route management + +## Feature Comparison + +| Feature | micro() | createMicroAPI() | +|---------|---------|------------------| +| Simple routing | โœ… | โœ… | +| Auto-response | โœ… | โŒ | +| Middleware | โœ… basic | โœ… pipeline | +| DI Container | โŒ | โœ… | +| Provider registration | โŒ | โœ… | + +## When to Use What + +### Use `micro()` when: +- Building simple APIs or serverless functions +- Prototyping quickly +- Don't need dependency injection +- Want minimal boilerplate + +### Use `createMicroAPI()` when: +- Building larger microservices +- Need dependency injection +- Need advanced middleware pipeline +- Building with provider pattern + +### Use Circuit Breaker when: +- Calling external/unreliable services +- Need fault tolerance +- Want to prevent cascading failures + +### Use Service Discovery when: +- Running multiple service instances +- Need load balancing +- Building microservice mesh + +### Use Service Client when: +- Service-to-service communication +- Need automatic retries +- Want circuit breaker on HTTP calls + +## Learn More + +- [ExpressoTS Documentation](https://expresso-ts.com) +- [Advanced Features Guide](../ADVANCED.md) +- [Upgrading Guide](../UPGRADING.md) diff --git a/micro/examples/circuit-breaker.example.ts b/micro/examples/circuit-breaker.example.ts new file mode 100644 index 0000000..171a51d --- /dev/null +++ b/micro/examples/circuit-breaker.example.ts @@ -0,0 +1,86 @@ +/** + * Circuit Breaker Example + * + * Demonstrates how to use the CircuitBreaker pattern to protect your + * microservice from cascading failures when calling external services. + * + * Run with: npm run example:circuit-breaker + */ + +import { micro, CircuitBreaker } from "@expressots/adapter-express"; + +const app = micro(); + +// Create a circuit breaker for external API calls +const externalApiBreaker = new CircuitBreaker({ + // Open circuit after 5 failures + failureThreshold: 5, + // Try to close circuit after 2 successes in half-open state + successThreshold: 2, + // Wait 30 seconds before trying to close an open circuit + timeout: 30000, + // Count failures within 10 second window + monitoringPeriod: 10000, +}); + +// Simulated external API call (replace with real API) +async function callExternalApi(): Promise<{ data: string }> { + // Simulate random failures for demo + if (Math.random() < 0.3) { + throw new Error("External API unavailable"); + } + return { data: "Success from external API" }; +} + +// Protected endpoint using circuit breaker +app.get("/api/external", async () => { + try { + const result = await externalApiBreaker.execute(async () => { + return await callExternalApi(); + }); + return result; + } catch (error) { + if (error instanceof Error && error.message === "Circuit breaker is OPEN") { + // Service is temporarily unavailable + return { + error: "Service temporarily unavailable", + message: "Please try again later", + retryAfter: 30, + }; + } + return { error: (error as Error).message }; + } +}); + +// Health check with circuit breaker status +app.get("/health", () => { + const stats = externalApiBreaker.getStats(); + return { + status: "healthy", + circuitBreaker: { + state: stats.state, + failures: stats.failures, + successes: stats.successes, + totalCalls: stats.totalCalls, + lastFailure: stats.lastFailure, + lastSuccess: stats.lastSuccess, + }, + }; +}); + +// Endpoint to manually reset the circuit breaker (for testing) +app.post("/admin/reset-circuit", () => { + externalApiBreaker.reset(); + return { message: "Circuit breaker reset" }; +}); + +// Endpoint to manually open the circuit (for maintenance) +app.post("/admin/open-circuit", () => { + externalApiBreaker.open(); + return { message: "Circuit breaker opened" }; +}); + +app.listen(3000, { + appName: "Circuit Breaker Example", + appVersion: "1.0.0", +}); diff --git a/micro/examples/full-di-api.example.ts b/micro/examples/full-di-api.example.ts new file mode 100644 index 0000000..34aca85 --- /dev/null +++ b/micro/examples/full-di-api.example.ts @@ -0,0 +1,139 @@ +/** + * Full DI API Example + * + * Demonstrates how to upgrade from the simple micro() API to the full + * createMicroAPI() with dependency injection container. + * + * Use this when you need: + * - Dependency injection + * - Middleware pipeline + * - Route management with prefixes + * - Provider registration + * + * Run with: npm run example:full-di-api + */ + +import { createMicroAPI } from "@expressots/adapter-express"; + +// Create a micro API with DI container +const microAPI = createMicroAPI(); + +// Set global route prefix (all routes will be prefixed with /api/v1) +microAPI.setGlobalRoutePrefix("/api/v1"); + +// Access the container for dependency injection +const container = microAPI.Container; + +// Register services in the container +// Example: Register a singleton service +class UserRepository { + private users = [ + { id: "1", name: "Alice", email: "alice@example.com" }, + { id: "2", name: "Bob", email: "bob@example.com" }, + ]; + + findAll() { + return this.users; + } + + findById(id: string) { + return this.users.find((u) => u.id === id); + } + + create(user: { name: string; email: string }) { + const newUser = { id: String(Date.now()), ...user }; + this.users.push(newUser); + return newUser; + } +} + +// Register as singleton (same instance throughout app lifecycle) +container.addSingleton(UserRepository); + +// Build the web server +const app = microAPI.build(); + +// Configure middleware +app.Middleware.parse(); // Enable JSON body parsing + +// Add custom middleware +app.Middleware.addMiddleware((req, _res, next) => { + console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`); + next(); +}); + +// Define routes using the Route interface +// Note: Routes are prefixed with /api/v1 + +app.Route.get("/", (req, res) => { + res.json({ + message: "Welcome to ExpressoTS Micro API with DI", + version: "1.0.0", + }); +}); + +app.Route.get("/users", (req, res) => { + // Get the repository from the container + const userRepo = container.get(UserRepository); + res.json(userRepo.findAll()); +}); + +app.Route.get("/users/:id", (req, res) => { + const userRepo = container.get(UserRepository); + const user = userRepo.findById(req.params.id); + + if (!user) { + res.status(404).json({ error: "User not found" }); + return; + } + + res.json(user); +}); + +app.Route.post("/users", (req, res) => { + const userRepo = container.get(UserRepository); + const user = userRepo.create(req.body); + res.status(201).json(user); +}); + +app.Route.get("/health", (req, res) => { + res.json({ + status: "healthy", + timestamp: new Date().toISOString(), + }); +}); + +// Set custom error handler +app.Middleware.setErrorHandler((err, req, res, next) => { + console.error("Error:", err); + res.status(500).json({ + error: err.message, + path: req.path, + }); +}); + +// Start the server +app.listen(3000, { + appName: "Full DI API Example", + appVersion: "1.0.0", +}); + +/** + * Feature Comparison: micro() vs createMicroAPI() + * + * | Feature | micro() | createMicroAPI() | + * |------------------------|-----------------|------------------| + * | Simple routing | โœ… | โœ… | + * | Auto-response | โœ… | โŒ | + * | Middleware | โœ… (basic) | โœ… (pipeline) | + * | DI Container | โŒ | โœ… | + * | Route prefix | โœ… (config) | โœ… | + * | Provider registration | โŒ | โœ… | + * | Middleware presets | โŒ | โœ… | + * + * When to upgrade: + * - Need dependency injection + * - Need advanced middleware pipeline + * - Need provider registration (singletons, etc.) + * - Building a larger microservice + */ diff --git a/micro/examples/serverless-lambda.example.ts b/micro/examples/serverless-lambda.example.ts new file mode 100644 index 0000000..3caeb27 --- /dev/null +++ b/micro/examples/serverless-lambda.example.ts @@ -0,0 +1,115 @@ +/** + * Serverless Lambda Example + * + * Demonstrates how to deploy an ExpressoTS Micro API to AWS Lambda. + * The same code works locally for development and on Lambda for production. + * + * Deployment: + * 1. Build: npm run build + * 2. Package: zip -r function.zip dist/ node_modules/ + * 3. Deploy to AWS Lambda with API Gateway + * + * For local development: npm run dev + */ + +import { micro, awsLambdaAdapter } from "@expressots/adapter-express"; + +// Create the micro API +const app = micro({ + showBanner: process.env.AWS_LAMBDA_FUNCTION_NAME ? false : true, +}); + +// Define your routes +app.get("/", () => { + return { + message: "Hello from ExpressoTS Micro on Lambda!", + environment: process.env.AWS_LAMBDA_FUNCTION_NAME ? "lambda" : "local", + }; +}); + +app.get("/users", () => { + return { + users: [ + { id: 1, name: "Alice" }, + { id: 2, name: "Bob" }, + ], + }; +}); + +app.get("/users/:id", (req) => { + return { + id: req.params.id, + name: `User ${req.params.id}`, + requestedAt: new Date().toISOString(), + }; +}); + +app.post("/users", (req) => { + return { + id: Date.now(), + ...req.body, + createdAt: new Date().toISOString(), + }; +}); + +app.get("/health", () => { + return { + status: "healthy", + timestamp: new Date().toISOString(), + memoryUsage: process.memoryUsage(), + lambdaContext: process.env.AWS_LAMBDA_FUNCTION_NAME + ? { + functionName: process.env.AWS_LAMBDA_FUNCTION_NAME, + memoryLimit: process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE, + region: process.env.AWS_REGION, + } + : null, + }; +}); + +// Error handling +app.setErrorHandler((err, _req, res) => { + console.error("Error:", err); + res.status(500).json({ + error: err.message, + stack: process.env.NODE_ENV === "development" ? err.stack : undefined, + }); +}); + +// Export for Lambda +// AWS Lambda will call this handler +export const handler = awsLambdaAdapter(app.getApp(), { + debug: process.env.DEBUG === "true", + binaryContentTypes: ["application/octet-stream", "image/*"], +}); + +// Local development +// Only starts the server when running locally (not on Lambda) +if (!process.env.AWS_LAMBDA_FUNCTION_NAME) { + app.listen(3000, { + appName: "Lambda Example", + appVersion: "1.0.0", + }); +} + +/** + * AWS SAM Template Example (template.yaml): + * + * AWSTemplateFormatVersion: '2010-09-09' + * Transform: AWS::Serverless-2016-10-31 + * + * Resources: + * ExpressoTSFunction: + * Type: AWS::Serverless::Function + * Properties: + * Handler: dist/examples/serverless-lambda.example.handler + * Runtime: nodejs20.x + * MemorySize: 256 + * Timeout: 30 + * Events: + * Api: + * Type: HttpApi + * Properties: + * Path: /{proxy+} + * Method: ANY + */ diff --git a/micro/examples/service-client.example.ts b/micro/examples/service-client.example.ts new file mode 100644 index 0000000..f791af7 --- /dev/null +++ b/micro/examples/service-client.example.ts @@ -0,0 +1,175 @@ +/** + * Service Client Example + * + * Demonstrates how to use ServiceClient for service-to-service communication + * with automatic retries, timeouts, and circuit breaker integration. + * + * Run with: npm run example:service-client + */ + +import { micro, ServiceClient } from "@expressots/adapter-express"; + +const app = micro(); + +// Create service clients for different microservices +const userService = new ServiceClient({ + name: "user-service", + baseUrl: "http://localhost:3001", + timeout: 5000, // 5 second timeout + retries: 3, // Retry failed requests up to 3 times + circuitBreaker: { + failureThreshold: 5, + timeout: 30000, + }, + headers: { + "X-Service-Name": "api-gateway", + }, +}); + +const orderService = new ServiceClient({ + name: "order-service", + baseUrl: "http://localhost:4001", + timeout: 10000, // Longer timeout for order processing + retries: 2, + circuitBreaker: true, // Use default circuit breaker config +}); + +// Disable circuit breaker for analytics (non-critical) +const analyticsService = new ServiceClient({ + name: "analytics-service", + baseUrl: "http://localhost:5001", + timeout: 3000, + retries: 1, + circuitBreaker: false, +}); + +// Get user by ID +app.get("/users/:id", async (req) => { + try { + interface User { + id: string; + name: string; + email: string; + } + const user = await userService.get(`/api/users/${req.params.id}`); + return user; + } catch (error) { + if ((error as Error).message.includes("Circuit breaker is OPEN")) { + return { error: "User service temporarily unavailable" }; + } + return { error: (error as Error).message }; + } +}); + +// Create a new user +app.post("/users", async (req) => { + try { + interface User { + id: string; + name: string; + email: string; + } + const user = await userService.post("/api/users", req.body); + return user; + } catch (error) { + return { error: (error as Error).message }; + } +}); + +// Create an order (calls user and order services) +app.post("/orders", async (req) => { + const { userId, items } = req.body; + + try { + // First verify user exists + interface User { + id: string; + name: string; + } + const user = await userService.get(`/api/users/${userId}`); + + // Then create the order + interface Order { + id: string; + userId: string; + items: unknown[]; + total: number; + } + const order = await orderService.post("/api/orders", { + userId, + items, + }); + + // Fire-and-forget analytics (don't wait, don't fail if it errors) + analyticsService + .post("/events", { + event: "order.created", + orderId: order.id, + userId, + timestamp: new Date().toISOString(), + }) + .catch(() => { + // Ignore analytics errors + }); + + return { order, user }; + } catch (error) { + return { error: (error as Error).message }; + } +}); + +// Get service health and statistics +app.get("/health", () => { + return { + status: "healthy", + services: { + user: userService.getStats(), + order: orderService.getStats(), + analytics: analyticsService.getStats(), + }, + }; +}); + +// Demonstrate custom headers per request +app.get("/users/:id/with-auth", async (req) => { + try { + interface User { + id: string; + name: string; + } + const user = await userService.get(`/api/users/${req.params.id}`, { + headers: { + Authorization: `Bearer ${req.headers.authorization}`, + "X-Request-ID": `req-${Date.now()}`, + }, + }); + return user; + } catch (error) { + return { error: (error as Error).message }; + } +}); + +// Demonstrate query parameters +app.get("/users", async (req) => { + try { + interface UserList { + users: unknown[]; + total: number; + } + const result = await userService.call("/api/users", { + params: { + page: (req.query.page as string) || "1", + limit: (req.query.limit as string) || "10", + sort: (req.query.sort as string) || "createdAt", + }, + }); + return result; + } catch (error) { + return { error: (error as Error).message }; + } +}); + +app.listen(3000, { + appName: "Service Client Example", + appVersion: "1.0.0", +}); diff --git a/micro/examples/service-discovery.example.ts b/micro/examples/service-discovery.example.ts new file mode 100644 index 0000000..7f3e843 --- /dev/null +++ b/micro/examples/service-discovery.example.ts @@ -0,0 +1,140 @@ +/** + * Service Discovery Example + * + * Demonstrates how to use ServiceDiscovery for registering and discovering + * microservices with round-robin load balancing. + * + * Run with: npm run example:service-discovery + */ + +import { micro, ServiceDiscovery, ServiceClient } from "@expressots/adapter-express"; + +const app = micro(); + +// Create a service discovery instance (static mode for this example) +const discovery = new ServiceDiscovery({ + type: "static", + debug: true, // Enable logging for demo +}); + +// Register some service instances +// In production, services would self-register on startup +discovery.registerService({ + id: "user-service-1", + name: "user-service", + host: "localhost", + port: 3001, + health: "healthy", + lastCheck: new Date(), + metadata: { version: "1.0.0", region: "us-east-1" }, +}); + +discovery.registerService({ + id: "user-service-2", + name: "user-service", + host: "localhost", + port: 3002, + health: "healthy", + lastCheck: new Date(), + metadata: { version: "1.0.0", region: "us-west-1" }, +}); + +discovery.registerService({ + id: "order-service-1", + name: "order-service", + host: "localhost", + port: 4001, + health: "healthy", + lastCheck: new Date(), +}); + +// Get users - demonstrates round-robin load balancing +app.get("/users", async () => { + const instance = discovery.getService("user-service"); + + if (!instance) { + return { error: "No healthy user-service instances available" }; + } + + // Create client for this instance + const client = new ServiceClient({ + name: "user-service", + baseUrl: `http://${instance.host}:${instance.port}`, + timeout: 5000, + }); + + try { + // In real scenario, make the actual call + // const users = await client.get("/api/users"); + return { + message: `Would call user-service at ${instance.host}:${instance.port}`, + instance: { + id: instance.id, + host: instance.host, + port: instance.port, + }, + }; + } catch (error) { + // Mark instance as unhealthy on failure + discovery.updateHealth(instance.name, instance.id, "unhealthy"); + return { error: (error as Error).message }; + } +}); + +// List all registered services +app.get("/services", () => { + const serviceNames = discovery.listServices(); + const services: Record = {}; + + for (const name of serviceNames) { + services[name] = discovery.getServiceInstances(name, false); + } + + return { services }; +}); + +// Get service statistics +app.get("/services/stats", () => { + return { stats: discovery.getStats() }; +}); + +// Register a new service instance (for self-registration) +app.post("/services/register", (req) => { + const { id, name, host, port, metadata } = req.body; + + discovery.registerService({ + id, + name, + host, + port, + health: "healthy", + lastCheck: new Date(), + metadata, + }); + + return { message: `Registered ${name} (${id})` }; +}); + +// Deregister a service instance +app.delete("/services/:name/:id", (req) => { + const { name, id } = req.params; + discovery.deregisterService(name, id); + return { message: `Deregistered ${name} (${id})` }; +}); + +// Update service health +app.put("/services/:name/:id/health", (req) => { + const { name, id } = req.params; + const { health } = req.body; + + const updated = discovery.updateHealth(name, id, health); + if (updated) { + return { message: `Updated ${name} (${id}) health to ${health}` }; + } + return { error: "Service instance not found" }; +}); + +app.listen(3000, { + appName: "Service Discovery Example", + appVersion: "1.0.0", +}); diff --git a/micro/package.json b/micro/package.json index 00cdd74..9829d7a 100644 --- a/micro/package.json +++ b/micro/package.json @@ -1,20 +1,41 @@ { "name": "expressots-micro", "version": "1.0.0", - "description": "ExpressoTS Microservice", + "description": "ExpressoTS Microservice - Lightweight API with Circuit Breaker, Service Discovery, and Serverless support", "author": "", "license": "MIT", "main": "dist/src/api.js", + "keywords": [ + "expressots", + "microservices", + "micro-api", + "circuit-breaker", + "service-discovery", + "service-mesh", + "serverless", + "aws-lambda", + "vercel", + "cloudflare-workers", + "api-gateway", + "load-balancing", + "fault-tolerance", + "typescript", + "express" + ], "scripts": { "build": "expressots build", "dev": "expressots dev", "prod": "expressots prod", "format": "prettier --write \"src/**/*.ts\"", - "lint": "eslint \"src/**/*.ts\" --fix" + "lint": "eslint \"src/**/*.ts\" --fix", + "example:circuit-breaker": "tsx examples/circuit-breaker.example.ts", + "example:service-discovery": "tsx examples/service-discovery.example.ts", + "example:service-client": "tsx examples/service-client.example.ts", + "example:full-di-api": "tsx examples/full-di-api.example.ts" }, "dependencies": { - "@expressots/adapter-express": "file:../../adapter-express/lib/expressots-adapter-express-4.0.0-beta.1.tgz", - "@expressots/core": "file:../../expressots/expressots-core-4.0.0-beta.1.tgz", + "@expressots/adapter-express": "^4.0.0", + "@expressots/core": "^4.0.0", "express": "^5.0.0", "reflect-metadata": "^0.2.2" }, @@ -22,6 +43,7 @@ "@types/express": "^5.0.0", "@types/node": "^22.10.2", "prettier": "^3.4.2", + "tsx": "^4.21.0", "typescript": "^5.7.2" } } From b792e26e7c4b4acd91b5b82fe457dd31fe6c0a0e Mon Sep 17 00:00:00 2001 From: Richard Zampieri Date: Tue, 13 Jan 2026 16:58:47 -0800 Subject: [PATCH 20/21] chore: clean up configuration and update dependencies for ExpressoTS Micro - Removed the .env.local file as it is no longer needed. - Updated entryPoint in expressots.config.ts to reflect the new directory structure. - Revised package.json to use local paths for dependencies and added new testing scripts. - Modified tsconfig files to improve build settings and include test files. - Changed the API listening port from 3001 to 3000 for consistency. --- micro/.env.local | 4 ---- micro/eslint.config.mjs | 43 ++++++++++++++++++++++++++++++++++ micro/expressots.config.ts | 2 +- micro/jest.config.ts | 17 ++++++++++++++ micro/package.json | 48 +++++++++++++++++--------------------- micro/src/api.ts | 2 +- micro/test/api.spec.ts | 29 +++++++++++++++++++++++ micro/tsconfig.build.json | 26 +++++++++------------ micro/tsconfig.json | 43 ++++++++++++++++++++++++++-------- 9 files changed, 157 insertions(+), 57 deletions(-) delete mode 100644 micro/.env.local create mode 100644 micro/eslint.config.mjs create mode 100644 micro/jest.config.ts create mode 100644 micro/test/api.spec.ts diff --git a/micro/.env.local b/micro/.env.local deleted file mode 100644 index c120e15..0000000 --- a/micro/.env.local +++ /dev/null @@ -1,4 +0,0 @@ -# Development Environment -APP_NAME=ExpressoTS Micro -APP_VERSION=1.0.0 -PORT=3001 diff --git a/micro/eslint.config.mjs b/micro/eslint.config.mjs new file mode 100644 index 0000000..f480369 --- /dev/null +++ b/micro/eslint.config.mjs @@ -0,0 +1,43 @@ +import js from "@eslint/js"; +import tseslint from "typescript-eslint"; +import { fileURLToPath } from "node:url"; + +const __dirname = fileURLToPath(new URL(".", import.meta.url)); + +export default tseslint.config( + js.configs.recommended, + ...tseslint.configs.recommended, + { + files: ["src/**/*.ts", "test/**/*.ts"], + languageOptions: { + parserOptions: { + project: true, + tsconfigRootDir: __dirname, + }, + }, + rules: { + // TypeScript-specific rules + "@typescript-eslint/no-unused-vars": [ + "error", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + }, + ], + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-unused-vars": "off", + "@typescript-eslint/no-non-null-assertion": "warn", + }, + }, + { + ignores: [ + "node_modules/**", + "dist/**", + "coverage/**", + "*.config.ts", + "*.config.js", + "*.config.mjs", + ], + }, +); diff --git a/micro/expressots.config.ts b/micro/expressots.config.ts index 3dc3853..90360e7 100644 --- a/micro/expressots.config.ts +++ b/micro/expressots.config.ts @@ -1,7 +1,7 @@ import { ExpressoConfig, Pattern } from "@expressots/shared"; const config: ExpressoConfig = { - entryPoint: "api", + entryPoint: "src/api", sourceRoot: "src", scaffoldPattern: Pattern.KEBAB_CASE, opinionated: false, diff --git a/micro/jest.config.ts b/micro/jest.config.ts new file mode 100644 index 0000000..251492b --- /dev/null +++ b/micro/jest.config.ts @@ -0,0 +1,17 @@ +import type { Config } from "jest"; + +const config: Config = { + preset: "ts-jest", + testEnvironment: "node", + rootDir: ".", + testMatch: ["/test/**/*.spec.ts"], + moduleNameMapper: { + "^@app/(.*)$": "/src/$1", + }, + modulePathIgnorePatterns: ["/dist"], + collectCoverageFrom: ["src/**/*.ts", "!src/**/*.d.ts", "!src/main.ts"], + coverageDirectory: "coverage", + coverageReporters: ["text", "lcov"], +}; + +export default config; diff --git a/micro/package.json b/micro/package.json index 9829d7a..4d8c437 100644 --- a/micro/package.json +++ b/micro/package.json @@ -5,45 +5,41 @@ "author": "", "license": "MIT", "main": "dist/src/api.js", - "keywords": [ - "expressots", - "microservices", - "micro-api", - "circuit-breaker", - "service-discovery", - "service-mesh", - "serverless", - "aws-lambda", - "vercel", - "cloudflare-workers", - "api-gateway", - "load-balancing", - "fault-tolerance", - "typescript", - "express" - ], "scripts": { "build": "expressots build", "dev": "expressots dev", "prod": "expressots prod", "format": "prettier --write \"src/**/*.ts\"", "lint": "eslint \"src/**/*.ts\" --fix", + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage", "example:circuit-breaker": "tsx examples/circuit-breaker.example.ts", "example:service-discovery": "tsx examples/service-discovery.example.ts", "example:service-client": "tsx examples/service-client.example.ts", "example:full-di-api": "tsx examples/full-di-api.example.ts" }, "dependencies": { - "@expressots/adapter-express": "^4.0.0", - "@expressots/core": "^4.0.0", - "express": "^5.0.0", - "reflect-metadata": "^0.2.2" + "@expressots/adapter-express": "file:../adapter-express/expressots-adapter-express-4.0.0-beta.1.tgz", + "@expressots/core": "file:../expressots/expressots-core-4.0.0-beta.1.tgz", + "@expressots/shared": "file:../shared/expressots-shared-4.0.0-beta.1.tgz", + "express": "5.2.1" }, "devDependencies": { - "@types/express": "^5.0.0", - "@types/node": "^22.10.2", - "prettier": "^3.4.2", - "tsx": "^4.21.0", - "typescript": "^5.7.2" + "@eslint/js": "9.39.2", + "@expressots/cli": "file:../expressots-cli/expressots-cli-4.0.0-beta.1.tgz", + "@types/express": "5.0.6", + "@types/jest": "30.0.0", + "@types/node": "25.0.3", + "@typescript-eslint/eslint-plugin": "8.51.0", + "@typescript-eslint/parser": "8.51.0", + "eslint": "9.39.2", + "jest": "30.2.0", + "nodemon": "3.1.11", + "prettier": "3.7.4", + "ts-jest": "29.4.6", + "tsx": "4.21.0", + "typescript": "5.9.3", + "typescript-eslint": "^8.53.0" } } diff --git a/micro/src/api.ts b/micro/src/api.ts index f7b1931..5f492ca 100644 --- a/micro/src/api.ts +++ b/micro/src/api.ts @@ -6,4 +6,4 @@ app.get("/", () => { return "Hello from ExpressoTS Micro API!"; }); -app.listen(3001); +app.listen(3000); diff --git a/micro/test/api.spec.ts b/micro/test/api.spec.ts new file mode 100644 index 0000000..9c77e11 --- /dev/null +++ b/micro/test/api.spec.ts @@ -0,0 +1,29 @@ +import { micro, MicroApp } from "@expressots/adapter-express"; +import { AddressInfo } from "net"; + +describe("Micro API", () => { + let api: MicroApp; + let baseUrl: string; + + beforeAll(async () => { + api = micro({ showBanner: false }); + api.get("/", () => "Hello from ExpressoTS Micro API!"); + await api.listen(0); + + const { port } = api.getHttpServer().address() as AddressInfo; + baseUrl = `http://localhost:${port}`; + }); + + afterAll(async () => { + await new Promise((resolve) => + api.getHttpServer().close(() => resolve()), + ); + }); + + it("should return hello message on GET /", async () => { + const response = await fetch(`${baseUrl}/`); + + expect(response.status).toBe(200); + expect(await response.text()).toBe("Hello from ExpressoTS Micro API!"); + }); +}); diff --git a/micro/tsconfig.build.json b/micro/tsconfig.build.json index 4764b76..3579133 100644 --- a/micro/tsconfig.build.json +++ b/micro/tsconfig.build.json @@ -1,22 +1,18 @@ { + "extends": "./tsconfig.json", "compilerOptions": { - "target": "ES2021", - "module": "commonjs", "outDir": "./dist", - "rootDir": "./src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "moduleResolution": "node", - "resolveJsonModule": true, + "rootDir": "./", + "declaration": false, + "declarationMap": false, + "sourceMap": false, "removeComments": true, - "noImplicitAny": false, - "strictPropertyInitialization": false, - "types": ["reflect-metadata", "node", "jest"], - "experimentalDecorators": true, - "emitDecoratorMetadata": true + "incremental": true, + "tsBuildInfoFile": "./dist/.tsbuildinfo", + "isolatedModules": true, + "types": ["node"], + "baseUrl": "." }, "include": ["src/**/*.ts"], - "exclude": ["node_modules", "dist", "test/**/*"] + "exclude": ["node_modules", "dist", "test"] } diff --git a/micro/tsconfig.json b/micro/tsconfig.json index 6ba3401..99d10f7 100644 --- a/micro/tsconfig.json +++ b/micro/tsconfig.json @@ -1,23 +1,46 @@ { "compilerOptions": { + // Language and Environment + "target": "ES2021", + "lib": ["ES2021"], + + // Modules "module": "commonjs", + "moduleResolution": "node", + "esModuleInterop": true, + "resolveJsonModule": true, + + // Emit + "outDir": "./dist", + "rootDir": "./", "declaration": true, + "declarationMap": true, + "sourceMap": true, "removeComments": true, + + // ExpressoTS Required: Decorators and Metadata "experimentalDecorators": true, "emitDecoratorMetadata": true, - "allowSyntheticDefaultImports": true, - "resolveJsonModule": true, - "target": "ES2021", - "sourceMap": true, - "rootDir": "./src", - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, + + // Path Mapping + "baseUrl": "./src", + + // Type Checking "strict": true, "noImplicitAny": false, "strictPropertyInitialization": false, "skipLibCheck": true, - "types": ["reflect-metadata", "node", "jest"] + + // Performance + "incremental": true, + "tsBuildInfoFile": "./dist/.tsbuildinfo", + + // Interop Constraints + "forceConsistentCasingInFileNames": true, + + // Type Definitions + "types": ["node", "jest"] }, - "include": ["src/**/*.ts"], - "exclude": ["node_modules", "dist", "test/**/*"] + "include": ["src/**/*.ts", "test/**/*.ts", "expressots.config.ts"], + "exclude": ["node_modules", "dist"] } From d9a1b145d05674eb64242e021962337a163d79cf Mon Sep 17 00:00:00 2001 From: Richard Zampieri Date: Fri, 24 Apr 2026 19:16:15 -0700 Subject: [PATCH 21/21] feat(v4): pin templates to @expressots/* ^4.0.0 - application/package.json: add missing @expressots/adapter-express, @expressots/core, @expressots/shared and reflect-metadata runtime deps, plus @expressots/cli devDependency. The new application template was missing every framework dep and would not boot. - provider/package.json: bump @expressots/core from 3.0.0 to ^4.0.0 so scaffolded providers compile against the v4 API. Made-with: Cursor --- application/package.json | 7 ++++++- provider/package.json | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/application/package.json b/application/package.json index cbc9dc3..1df5f32 100644 --- a/application/package.json +++ b/application/package.json @@ -16,10 +16,15 @@ "lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\" --fix" }, "dependencies": { - "express": "5.2.1" + "@expressots/adapter-express": "^4.0.0", + "@expressots/core": "^4.0.0", + "@expressots/shared": "^4.0.0", + "express": "5.2.1", + "reflect-metadata": "^0.2.2" }, "devDependencies": { "@eslint/js": "9.39.2", + "@expressots/cli": "^4.0.0", "@types/express": "5.0.6", "@types/jest": "30.0.0", "@types/node": "25.0.3", diff --git a/provider/package.json b/provider/package.json index 14f8b22..d59cf0d 100644 --- a/provider/package.json +++ b/provider/package.json @@ -64,7 +64,7 @@ "lint:fix": "eslint \"src/**/*.ts\" --fix" }, "dependencies": { - "@expressots/core": "3.0.0" + "@expressots/core": "^4.0.0" }, "devDependencies": { "@commitlint/cli": "19.5.0",