├── .dockerignore ├── .env.example ├── .env.test ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CONTRIBUTING.md ├── Dockerfile ├── Dockerfile.dev ├── LICENSE ├── README.md ├── assets └── express-api-dashboard-grafana.gif ├── data ├── alertmanager │ └── alertmanager.yml ├── grafana │ ├── dashboards │ │ └── default │ │ │ └── express-dashboard.json │ └── provisioning │ │ ├── dashboards │ │ └── default.yml │ │ └── datasources │ │ └── prometheus.yml ├── prometheus │ └── prometheus.yml └── rules │ ├── alerting_rules.yml │ └── recording_rules.yml ├── docker-compose.dev.yml ├── docker-compose.yml ├── jest.config.ts ├── jest.e2e.config.ts ├── nodemon.json ├── package-lock.json ├── package.json ├── prisma ├── migrations │ ├── 20250110225528_init │ │ └── migration.sql │ └── migration_lock.toml └── schema.prisma ├── requests ├── auth │ ├── auth.rest │ ├── email-tests.rest │ └── password-reset.rest ├── monitoring │ └── monitoring.rest └── users │ └── users.rest ├── scripts ├── dev.seed.ts └── prod.seed.ts ├── src ├── @types │ ├── express │ │ └── index.d.ts │ └── global.d.ts ├── __tests__ │ ├── e2e │ │ └── auth.e2e.test.ts │ ├── helpers │ │ └── user.helper.ts │ ├── setup.e2e.ts │ └── websocket │ │ ├── jest.websocket.config.ts │ │ └── websocket.test.ts ├── app.ts ├── config │ ├── database.ts │ ├── env.ts │ └── logger.ts ├── controllers │ ├── auth.controller.ts │ ├── base.controller.ts │ ├── monitoring.controller.ts │ └── user.controller.ts ├── decorators │ └── singleton.ts ├── docs │ └── swagger.ts ├── index.ts ├── middleware │ ├── authMiddleware.ts │ ├── cacheMiddleware.ts │ ├── errorHandler.ts │ ├── loggingMiddleware.ts │ ├── monitoringMiddleware.ts │ ├── notFound.ts │ ├── performanceMiddleware.ts │ ├── rateLimiter.ts │ ├── requestId.ts │ ├── securityHeaders.ts │ └── validateRequest.ts ├── routes │ ├── auth.routes.ts │ ├── monitoring.routes.ts │ └── user.routes.ts ├── services │ ├── auth.service.ts │ ├── email.service.ts │ ├── errorMonitoring.service.ts │ ├── metrics.service.ts │ ├── user.service.ts │ └── websocket.service.ts ├── templates │ └── emails │ │ ├── index.ts │ │ ├── reset-password.template.ts │ │ └── verification.template.ts ├── utils │ ├── apiResponse.ts │ ├── appError.ts │ ├── errorCodes.ts │ └── errorHandler.ts └── validators │ ├── auth.validator.ts │ └── user.validator.ts ├── tsconfig.json └── tsconfig.scripts.json /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | Dockerfile* 4 | .dockerignore 5 | .git 6 | .gitignore 7 | README.md 8 | coverage 9 | logs -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Server 2 | NODE_ENV=development 3 | PORT=4300 4 | SERVER_URL=http://localhost:4300 5 | 6 | # Database 7 | MYSQL_DATABASE_URL=mysql://express-boilerplate:express-boilerplate@localhost:3306/express-boilerplate 8 | 9 | # JWT 10 | JWT_SECRET=your-jwt-secret-min-32-chars 11 | REFRESH_TOKEN_SECRET=your-refresh-token-secret-min-32-chars 12 | JWT_EXPIRY=15m 13 | REFRESH_TOKEN_EXPIRY=7d 14 | 15 | # Frontend 16 | FRONTEND_URL=http://localhost:3000 17 | 18 | # Prometheus 19 | PROMETHEUS_URL=http://localhost:9090 20 | 21 | # Email (Optional in development) 22 | SMTP_HOST=smtp.example.com 23 | SMTP_PORT=587 24 | SMTP_USER=your-smtp-user 25 | SMTP_PASSWORD=your-smtp-password 26 | SMTP_FROM=no-reply@example.com 27 | 28 | # App 29 | APP_NAME=Express Boilerplate -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | # Database 2 | MYSQL_DATABASE_URL=mysql://root@localhost:3306/express-boilerplate 3 | 4 | # Server 5 | PORT=4300 6 | NODE_ENV=test 7 | 8 | # Authentication 9 | JWT_SECRET=your-32-character-jwt-secret-key-here-123 10 | REFRESH_TOKEN_SECRET=your-32-character-refresh-token-key-here 11 | JWT_EXPIRY=15m 12 | REFRESH_TOKEN_EXPIRY=7d 13 | 14 | # CORS 15 | FRONTEND_URL=http://localhost:3000 16 | 17 | # Prometheus 18 | PROMETHEUS_URL=http://localhost:9090 19 | 20 | # Server 21 | SERVER_URL=http://localhost:4300 22 | 23 | # SMTP Configuration (not needed in development when using Ethereal) 24 | SMTP_HOST=smtp.example.com 25 | SMTP_PORT=587 26 | SMTP_USER=your-smtp-user 27 | SMTP_PASSWORD=your-smtp-password 28 | SMTP_FROM=noreply@example.com 29 | APP_NAME="Express Boilerplate" -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI/CD 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | services: 14 | mysql: 15 | image: mysql:8.0 16 | env: 17 | MYSQL_ROOT_PASSWORD: rootpassword 18 | MYSQL_DATABASE: express-boilerplate 19 | ports: 20 | - 3306:3306 21 | options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 22 | 23 | steps: 24 | - uses: actions/checkout@v3 25 | 26 | - name: Setup Node.js 27 | uses: actions/setup-node@v3 28 | with: 29 | node-version: "22" 30 | cache: "npm" 31 | 32 | - name: Install dependencies 33 | run: npm ci 34 | 35 | - name: Run tests 36 | run: | 37 | cp .env.example .env 38 | npm run test 39 | env: 40 | MYSQL_DATABASE_URL: mysql://root@localhost:3306/express-boilerplate 41 | NODE_ENV: test 42 | JWT_SECRET: test-jwt-secret 43 | REFRESH_TOKEN_SECRET: test-refresh-token-secret 44 | JWT_EXPIRY: 15m 45 | REFRESH_TOKEN_EXPIRY: 7d 46 | FRONTEND_URL: http://localhost:3000 47 | 48 | build: 49 | needs: test 50 | runs-on: ubuntu-latest 51 | if: github.ref == 'refs/heads/main' 52 | 53 | steps: 54 | - uses: actions/checkout@v3 55 | 56 | - name: Login to Docker Hub 57 | uses: docker/login-action@v2 58 | with: 59 | username: ${{ secrets.DOCKERHUB_USERNAME }} 60 | password: ${{ secrets.DOCKERHUB_TOKEN }} 61 | 62 | - name: Build and push Docker image 63 | uses: docker/build-push-action@v4 64 | with: 65 | push: true 66 | tags: your-username/express-boilerplate:latest 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | dist 4 | logs/ 5 | *.log 6 | coverage/ -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Express TypeScript Boilerplate 2 | 3 | First off, thank you for considering contributing to this project! 🎉 4 | 5 | ## Table of Contents 6 | 7 | - [Code of Conduct](#code-of-conduct) 8 | - [Getting Started](#getting-started) 9 | - [Development Process](#development-process) 10 | - [Pull Request Process](#pull-request-process) 11 | - [Coding Standards](#coding-standards) 12 | - [Commit Guidelines](#commit-guidelines) 13 | - [Running Tests](#running-tests) 14 | 15 | ## Code of Conduct 16 | 17 | This project adheres to a Code of Conduct that all contributors are expected to follow. By participating, you are expected to uphold this code. 18 | 19 | ### Our Standards 20 | 21 | - Using welcoming and inclusive language 22 | - Being respectful of differing viewpoints and experiences 23 | - Gracefully accepting constructive criticism 24 | - Focusing on what is best for the community 25 | - Showing empathy towards other community members 26 | 27 | ## Getting Started 28 | 29 | 1. Fork the repository 30 | 2. Clone your fork: 31 | ```bash 32 | git clone https://github.com/mzubair481/express-boilerplate.git 33 | ``` 34 | 3. Create a new branch: 35 | ```bash 36 | git checkout -b feature/your-feature-name 37 | ``` 38 | 4. Set up development environment: 39 | ```bash 40 | npm install 41 | cp .env.example .env 42 | ``` 43 | 44 | ## Development Process 45 | 46 | 1. Create a feature branch from `main` 47 | 2. Make your changes 48 | 3. Write or update tests 49 | 4. Update documentation 50 | 5. Submit a pull request 51 | 52 | ### Branch Naming Convention 53 | 54 | - Feature: `feature/your-feature-name` 55 | - Bug fix: `fix/issue-description` 56 | - Documentation: `docs/what-you-documented` 57 | - Performance: `perf/what-you-optimized` 58 | 59 | ## Pull Request Process 60 | 61 | 1. Update the README.md with details of changes if applicable 62 | 2. Update the documentation 63 | 3. Add tests for new functionality 64 | 4. Ensure the test suite passes 65 | 5. Update the CHANGELOG.md 66 | 6. The PR must be reviewed by at least one maintainer 67 | 68 | ### PR Title Format 69 | 70 | ``` 71 | type(scope): description 72 | 73 | Examples: 74 | feat(auth): add refresh token functionality 75 | fix(database): resolve connection pooling issue 76 | docs(readme): update deployment instructions 77 | ``` 78 | 79 | ## Coding Standards 80 | 81 | ### TypeScript 82 | 83 | - Use TypeScript's strict mode 84 | - Properly type all functions and variables 85 | - Use interfaces over types when possible 86 | - Document complex functions with JSDoc comments 87 | 88 | ### Code Style 89 | 90 | - Use 2 spaces for indentation 91 | - Use single quotes for strings 92 | - Add trailing commas in objects and arrays 93 | - Use meaningful variable names 94 | - Keep functions small and focused 95 | - Use async/await over raw promises 96 | 97 | ## Commit Guidelines 98 | 99 | We follow [Conventional Commits](https://www.conventionalcommits.org/): 100 | 101 | ``` 102 | (): 103 | 104 | [optional body] 105 | 106 | [optional footer(s)] 107 | ``` 108 | 109 | ### Types 110 | 111 | - `feat`: New feature 112 | - `fix`: Bug fix 113 | - `docs`: Documentation changes 114 | - `style`: Code style changes (formatting, etc) 115 | - `refactor`: Code refactoring 116 | - `perf`: Performance improvements 117 | - `test`: Adding or updating tests 118 | - `chore`: Maintenance tasks 119 | 120 | ## Running Tests 121 | 122 | Before submitting a PR, ensure all tests pass: 123 | 124 | ```bash 125 | # Run all tests 126 | npm test 127 | 128 | # Run E2E tests 129 | npm run test:e2e 130 | 131 | # Check test coverage 132 | npm run test:coverage 133 | ``` 134 | 135 | ## Questions or Problems? 136 | 137 | - Open an issue for bugs 138 | - Use discussions for questions 139 | - Tag maintainers for urgent issues 140 | 141 | Thank you for contributing! 🚀 142 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build Stage 2 | FROM node:18-alpine as builder 3 | 4 | # Add non-root user 5 | RUN addgroup -S appgroup && adduser -S appuser -G appgroup 6 | 7 | WORKDIR /app 8 | 9 | # Copy only necessary files first 10 | COPY package*.json ./ 11 | COPY prisma ./prisma/ 12 | COPY tsconfig*.json ./ 13 | COPY .env.example ./.env 14 | 15 | # Install dependencies 16 | RUN npm ci --only=production 17 | 18 | # Copy source code 19 | COPY . . 20 | RUN npm run build 21 | 22 | # Production Stage 23 | FROM node:18-alpine 24 | 25 | # Add non-root user 26 | RUN addgroup -S appgroup && adduser -S appuser -G appgroup 27 | 28 | WORKDIR /app 29 | 30 | COPY --from=builder --chown=appuser:appgroup /app/dist ./dist 31 | COPY --from=builder --chown=appuser:appgroup /app/node_modules ./node_modules 32 | COPY --from=builder --chown=appuser:appgroup /app/package*.json ./ 33 | COPY --from=builder --chown=appuser:appgroup /app/prisma ./prisma 34 | COPY --from=builder --chown=appuser:appgroup /app/.env ./ 35 | 36 | # Switch to non-root user 37 | USER appuser 38 | 39 | EXPOSE 4300 40 | 41 | # Health check 42 | HEALTHCHECK --interval=30s --timeout=3s --start-period=30s \ 43 | CMD wget --no-verbose --tries=1 --spider http://localhost:4300/health || exit 1 44 | 45 | CMD ["npm", "start"] -------------------------------------------------------------------------------- /Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine 2 | 3 | # Add non-root user 4 | RUN addgroup -S appgroup && adduser -S appuser -G appgroup 5 | 6 | # Install necessary tools 7 | RUN apk add --no-cache netcat-openbsd wget 8 | 9 | WORKDIR /app 10 | 11 | # Create directories and set permissions 12 | RUN mkdir -p node_modules dist && \ 13 | chown -R appuser:appgroup /app 14 | 15 | # Copy package files 16 | COPY package*.json ./ 17 | COPY prisma ./prisma/ 18 | COPY tsconfig*.json ./ 19 | COPY scripts ./scripts/ 20 | COPY nodemon.json ./ 21 | 22 | # Set permissions 23 | RUN chown -R appuser:appgroup /app 24 | 25 | # Switch to non-root user 26 | USER appuser 27 | 28 | # Install ALL dependencies (including devDependencies) 29 | RUN npm install 30 | 31 | EXPOSE 4300 32 | 33 | # Update CMD to use npx for running nodemon 34 | CMD ["npx", "nodemon"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Express TypeScript Boilerplate 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Express TypeScript Boilerplate 2 | 3 | 🚀 A production-ready Express.js boilerplate with TypeScript, featuring robust authentication, logging, monitoring, and best practices for building secure and scalable APIs. 4 | 5 | > If this boilerplate helps your project take off, consider giving it a ⭐️ - it fuels my cosmic journey! 🌠 6 | 7 | # Dashboard Demo 8 | 9 | ![Dashboard Demo](./assets/express-api-dashboard-grafana.gif) 10 | 11 | ## Features 12 | 13 | - **TypeScript** - Strongly typed language for better developer experience 14 | - **Authentication & Authorization** - JWT-based auth with refresh tokens 15 | - **Database Integration** - Prisma ORM with MySQL 16 | - **API Documentation** - REST client files for API testing 17 | - **Security** 18 | - Helmet for security headers 19 | - Rate limiting 20 | - CORS configuration 21 | - Request validation using Zod 22 | - **Monitoring & Logging** 23 | - Prometheus metrics 24 | - Grafana dashboards 25 | - Winston logger with daily rotate 26 | - Request ID tracking 27 | - **Performance** 28 | - Response compression 29 | - Caching middleware 30 | - Database connection pooling 31 | - **Testing** 32 | - Jest for unit and integration tests 33 | - E2E testing setup 34 | - Test helpers and utilities 35 | - **Docker Support** 36 | - Multi-stage builds 37 | - Docker Compose for local development 38 | - Health checks 39 | - **CI/CD** 40 | - GitHub Actions workflow 41 | - Automated testing 42 | - Docker image publishing 43 | 44 | ## Prerequisites 45 | 46 | - Node.js (v18 or higher) 47 | - MySQL (v8.0 or higher) 48 | - Docker and Docker Compose (optional) 49 | 50 | ## Getting Started 51 | 52 | ### Local Development 53 | 54 | 1. Clone the repository: 55 | ```bash 56 | git clone https://github.com/mzubair481/express-boilerplate.git 57 | cd express-boilerplate 58 | ``` 59 | 60 | 2. Install dependencies: 61 | ```bash 62 | npm install 63 | ``` 64 | 65 | 3. Set up environment variables: 66 | ```bash 67 | cp .env.example .env 68 | ``` 69 | 70 | 4. Set up the database: 71 | ```bash 72 | npm run migrate:dev 73 | npm run seed:dev 74 | ``` 75 | 76 | 5. Start the development server: 77 | ```bash 78 | npm run dev 79 | ``` 80 | 81 | ### Docker Setup 82 | 83 | Run the entire stack using Docker Compose: 84 | 85 | ```bash 86 | # Start all services 87 | npm run docker:dev 88 | 89 | # View logs 90 | npm run docker:dev:logs 91 | 92 | # Rebuild and start services 93 | npm run docker:dev:build 94 | 95 | # Stop services and remove volumes 96 | npm run docker:dev:down 97 | ``` 98 | 99 | #### Development with Hot Reload 100 | 101 | The development environment is configured with: 102 | - Nodemon for automatic server restart 103 | - Volume mounts for real-time code changes 104 | - TypeScript compilation on save 105 | - Environment variables for development 106 | 107 | ```yaml 108 | # Key configurations in docker-compose.dev.yml 109 | services: 110 | api: 111 | volumes: 112 | - ./src:/app/src:delegated # Source code 113 | - ./prisma:/app/prisma:delegated # Prisma schema 114 | - api_node_modules:/app/node_modules 115 | environment: 116 | - NODE_ENV=development 117 | - CHOKIDAR_USEPOLLING=true 118 | - CHOKIDAR_INTERVAL=1000 119 | ``` 120 | 121 | This will start: 122 | - Express API server (http://localhost:4300) 123 | - MySQL database (port 3306) 124 | - Prometheus metrics (http://localhost:9090) 125 | - Grafana dashboards (http://localhost:3000) 126 | - Node Exporter (system metrics) 127 | - Alertmanager (alerts management) 128 | 129 | ### Accessing Monitoring Tools 130 | 131 | 1. **Grafana**: 132 | - URL: http://localhost:3000 133 | - Default credentials: 134 | - Username: `admin` 135 | - Password: `admin` 136 | - Pre-configured dashboards: 137 | - API Metrics Dashboard 138 | - System Metrics Dashboard 139 | 140 | 2. **Prometheus**: 141 | - URL: http://localhost:9090 142 | - Metrics endpoint: http://localhost:4300/monitoring/metrics 143 | 144 | 3. **Alertmanager**: 145 | - URL: http://localhost:9093 146 | 147 | ### Monitoring Features 148 | 149 | - Real-time metrics visualization 150 | - Request rate and latency tracking 151 | - Error rate monitoring 152 | - Garbage Collection metrics 153 | - Node.js process statistics 154 | - CPU and memory usage 155 | - Custom alerts configuration 156 | - System metrics via Node Exporter 157 | - Automated alert notifications 158 | 159 | ## Available Scripts 160 | 161 | - `npm run dev` - Start development server 162 | - `npm run build` - Build for production 163 | - `npm start` - Start production server with GC metrics enabled 164 | - `npm test` - Run tests 165 | - `npm run test:e2e` - Run E2E tests 166 | - `npm run test:coverage` - Generate test coverage 167 | - `npm run migrate:dev` - Run database migrations 168 | - `npm run seed:dev` - Seed database with test data 169 | - `npm run studio` - Open Prisma Studio 170 | 171 | ### Docker Commands 172 | 173 | ```bash 174 | # Build and start services 175 | docker-compose up -d --build 176 | 177 | # Stop services 178 | docker-compose down 179 | 180 | # View logs 181 | docker-compose logs -f [service] 182 | 183 | # Restart a service 184 | docker-compose restart [service] 185 | 186 | # Remove volumes (database data) 187 | docker-compose down -v 188 | ``` 189 | 190 | ## Project Structure 191 | 192 | ``` 193 | ├── src/ 194 | │ ├── __tests__/ # Test files 195 | │ ├── @types/ # TypeScript type definitions 196 | │ ├── config/ # Configuration files 197 | │ ├── controllers/ # Route controllers 198 | │ ├── middleware/ # Express middleware 199 | │ ├── routes/ # API routes 200 | │ ├── services/ # Business logic 201 | │ ├── utils/ # Utility functions 202 | │ ├── validators/ # Request validation schemas 203 | │ ├── app.ts # Express app setup 204 | │ └── index.ts # Application entry point 205 | ├── prisma/ # Prisma schema and migrations 206 | ├── requests/ # REST client files 207 | └── docker/ # Docker configuration files 208 | ``` 209 | 210 | ## API Documentation 211 | 212 | The API is fully documented using OpenAPI/Swagger. You can access the interactive documentation at: 213 | 214 | ``` 215 | http://localhost:4300/api-docs 216 | ``` 217 | 218 | ### API Endpoints 219 | 220 | #### Authentication 221 | - `POST /api/auth/signup` - Register new user 222 | - `POST /api/auth/login` - User login 223 | - `POST /api/auth/refresh` - Refresh access token 224 | - `POST /api/auth/logout` - User logout 225 | - `GET /api/auth/verify-email/:token` - Verify email 226 | - `POST /api/auth/send-email-verification` - Resend verification email 227 | - `POST /api/auth/forgot-password` - Request password reset 228 | - `POST /api/auth/reset-password/:token` - Reset password 229 | 230 | #### Users 231 | - `GET /api/users` - Get all users (Admin only) 232 | - `GET /api/users/:id` - Get user by ID 233 | - `POST /api/users` - Create user (Admin only) 234 | - `PATCH /api/users/:id` - Update user 235 | - `DELETE /api/users/:id` - Delete user (Admin only) 236 | 237 | #### Monitoring 238 | - `GET /health` - Service health check 239 | - `GET /api/monitoring/metrics` - Prometheus metrics 240 | - `GET /api/monitoring/readiness` - Readiness probe 241 | - `GET /api/monitoring/liveness` - Liveness probe 242 | 243 | ### Authentication 244 | 245 | All protected endpoints require a valid JWT token in the Authorization header: 246 | 247 | ``` 248 | Authorization: Bearer 249 | ``` 250 | 251 | ### Request/Response Examples 252 | 253 | #### User Registration 254 | ```json 255 | POST /api/auth/signup 256 | { 257 | "email": "user@example.com", 258 | "password": "SecurePass123!", 259 | "name": "John Doe" 260 | } 261 | 262 | Response 201: 263 | { 264 | "success": true, 265 | "message": "User registered successfully", 266 | "data": { 267 | "id": "uuid", 268 | "email": "user@example.com", 269 | "name": "John Doe" 270 | } 271 | } 272 | ``` 273 | 274 | #### User Login 275 | ```json 276 | POST /api/auth/login 277 | { 278 | "email": "user@example.com", 279 | "password": "SecurePass123!" 280 | } 281 | 282 | Response 200: 283 | { 284 | "success": true, 285 | "message": "Login successful", 286 | "data": { 287 | "accessToken": "jwt_token", 288 | "refreshToken": "refresh_token" 289 | } 290 | } 291 | ``` 292 | 293 | ### Error Responses 294 | 295 | The API uses standardized error responses: 296 | 297 | ```json 298 | { 299 | "success": false, 300 | "message": "Error message", 301 | "code": "ERR_XXXX", 302 | "stack": "Error stack trace (development only)" 303 | } 304 | ``` 305 | 306 | ### Rate Limiting 307 | 308 | - Auth endpoints: 5 requests per minute 309 | - API endpoints: 100 requests per minute 310 | - WebSocket connections: 60 messages per minute 311 | 312 | ### Monitoring 313 | 314 | The API provides Prometheus metrics at `/api/monitoring/metrics` including: 315 | - HTTP request duration 316 | - Request counts by endpoint 317 | - Error rates 318 | - Active WebSocket connections 319 | - System metrics 320 | 321 | ### Authentication Flow 322 | 323 | 1. User signs up → Verification email sent 324 | 2. User verifies email via link 325 | 3. User can now login 326 | 4. Login returns access & refresh tokens 327 | 5. Use access token in Authorization header: `Bearer ` 328 | 329 | ### Password Reset Flow: 330 | 331 | 1. User requests password reset → Reset email sent 332 | 2. User clicks reset link in email 333 | 3. User sets new password using reset token 334 | 4. User can login with new password 335 | 336 | ### WebSocket API: 337 | 338 | - Connection 339 | - URL: `ws://localhost:4300` 340 | - Secure URL: `wss://your-domain.com` (production) 341 | 342 | - Message Format: 343 | ```javascript 344 | { 345 | "type": "message_type", 346 | "data": { 347 | // message payload 348 | } 349 | } 350 | ``` 351 | 352 | - Supported Message Types: 353 | - `ping` - Health check ping 354 | ```javascript 355 | // Client -> Server 356 | { "type": "ping" } 357 | 358 | // Server -> Client 359 | { 360 | "type": "pong", 361 | "data": { "timestamp": 1234567890 } 362 | } 363 | ``` 364 | - `connection` - Initial connection confirmation 365 | ```javascript 366 | // Server -> Client 367 | { 368 | "type": "connection", 369 | "data": { 370 | "clientId": "abc123", 371 | "message": "Connected to WebSocket server" 372 | } 373 | } 374 | ``` 375 | 376 | ### WebSocket Usage Example: 377 | 378 | ```javascript 379 | // Connect to WebSocket server 380 | const ws = new WebSocket('ws://localhost:4300'); 381 | 382 | // Handle connection open 383 | ws.onopen = () => { 384 | console.log('Connected to WebSocket server'); 385 | }; 386 | 387 | // Handle incoming messages 388 | ws.onmessage = (event) => { 389 | const message = JSON.parse(event.data); 390 | console.log('Received:', message); 391 | 392 | // Handle different message types 393 | switch (message.type) { 394 | case 'connection': 395 | console.log('Connected with ID:', message.data.clientId); 396 | break; 397 | case 'pong': 398 | console.log('Ping response received'); 399 | break; 400 | } 401 | }; 402 | 403 | // Handle errors 404 | ws.onerror = (error) => { 405 | console.error('WebSocket error:', error); 406 | }; 407 | 408 | // Handle connection close 409 | ws.onclose = () => { 410 | console.log('Disconnected from WebSocket server'); 411 | }; 412 | 413 | // Send a ping message 414 | ws.send(JSON.stringify({ type: 'ping' })); 415 | ``` 416 | 417 | ### WebSocket Security: 418 | 419 | - Authentication is required for certain message types 420 | - Rate limiting is applied to prevent abuse 421 | - Messages are validated for proper format and content 422 | - Connections are automatically closed after prolonged inactivity 423 | - SSL/TLS encryption is required in production 424 | 425 | ### Error Codes 426 | 427 | The application uses structured error codes for better error handling: 428 | 429 | - 1xxx: Authentication Errors (e.g., ERR_1001) 430 | - 2xxx: Authorization Errors 431 | - 3xxx: Validation Errors 432 | - 4xxx: Resource Errors 433 | - 5xxx: Database Errors 434 | - 6xxx: Server Errors 435 | 436 | ## Contributing 437 | 438 | Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull requests. 439 | 440 | ## License 441 | 442 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. -------------------------------------------------------------------------------- /assets/express-api-dashboard-grafana.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mzubair481/express-boilerplate/661c531c153d664a7c31226f7f111bb7c8d349e4/assets/express-api-dashboard-grafana.gif -------------------------------------------------------------------------------- /data/alertmanager/alertmanager.yml: -------------------------------------------------------------------------------- 1 | global: 2 | resolve_timeout: 5m 3 | 4 | route: 5 | group_by: ['alertname'] 6 | group_wait: 10s 7 | group_interval: 10s 8 | repeat_interval: 1h 9 | receiver: 'web.hook' 10 | 11 | receivers: 12 | - name: 'web.hook' 13 | webhook_configs: 14 | - url: 'http://api:4300/monitoring/alerts' 15 | send_resolved: true 16 | 17 | inhibit_rules: 18 | - source_match: 19 | severity: 'critical' 20 | target_match: 21 | severity: 'warning' 22 | equal: ['alertname'] -------------------------------------------------------------------------------- /data/grafana/dashboards/default/express-dashboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "annotations": { 3 | "list": [ 4 | { 5 | "builtIn": 1, 6 | "datasource": { 7 | "type": "grafana", 8 | "uid": "-- Grafana --" 9 | }, 10 | "enable": true, 11 | "hide": true, 12 | "iconColor": "rgba(0, 211, 255, 1)", 13 | "name": "Annotations & Alerts", 14 | "type": "dashboard" 15 | } 16 | ] 17 | }, 18 | "editable": true, 19 | "fiscalYearStartMonth": 0, 20 | "graphTooltip": 0, 21 | "id": 2, 22 | "links": [], 23 | "panels": [ 24 | { 25 | "datasource": { 26 | "type": "prometheus", 27 | "uid": "prometheus" 28 | }, 29 | "description": "Shows how many HTTP requests per second your API receives, broken down by HTTP method (GET, POST, etc.) and endpoint path", 30 | "fieldConfig": { 31 | "defaults": { 32 | "color": { 33 | "mode": "palette-classic" 34 | }, 35 | "custom": { 36 | "axisBorderShow": false, 37 | "axisCenteredZero": false, 38 | "axisColorMode": "text", 39 | "axisLabel": "", 40 | "axisPlacement": "auto", 41 | "barAlignment": 0, 42 | "barWidthFactor": 0.6, 43 | "drawStyle": "line", 44 | "fillOpacity": 10, 45 | "gradientMode": "none", 46 | "hideFrom": { 47 | "legend": false, 48 | "tooltip": false, 49 | "viz": false 50 | }, 51 | "insertNulls": false, 52 | "lineInterpolation": "linear", 53 | "lineWidth": 1, 54 | "pointSize": 5, 55 | "scaleDistribution": { 56 | "type": "linear" 57 | }, 58 | "showPoints": "never", 59 | "spanNulls": false, 60 | "stacking": { 61 | "group": "A", 62 | "mode": "none" 63 | }, 64 | "thresholdsStyle": { 65 | "mode": "off" 66 | } 67 | }, 68 | "mappings": [], 69 | "thresholds": { 70 | "mode": "absolute", 71 | "steps": [ 72 | { 73 | "color": "green", 74 | "value": null 75 | }, 76 | { 77 | "color": "red", 78 | "value": 80 79 | } 80 | ] 81 | } 82 | }, 83 | "overrides": [] 84 | }, 85 | "gridPos": { 86 | "h": 8, 87 | "w": 12, 88 | "x": 0, 89 | "y": 0 90 | }, 91 | "id": 1, 92 | "options": { 93 | "legend": { 94 | "calcs": [ 95 | "mean", 96 | "max" 97 | ], 98 | "displayMode": "table", 99 | "placement": "bottom", 100 | "showLegend": true 101 | }, 102 | "tooltip": { 103 | "mode": "single", 104 | "sort": "none" 105 | } 106 | }, 107 | "pluginVersion": "11.4.0", 108 | "targets": [ 109 | { 110 | "datasource": { 111 | "type": "prometheus", 112 | "uid": "prometheus" 113 | }, 114 | "expr": "rate(http_requests_total[5m])", 115 | "legendFormat": "{{method}} {{route}}", 116 | "refId": "A" 117 | } 118 | ], 119 | "title": "HTTP Request Rate", 120 | "type": "timeseries" 121 | }, 122 | { 123 | "datasource": { 124 | "type": "prometheus", 125 | "uid": "prometheus" 126 | }, 127 | "fieldConfig": { 128 | "defaults": { 129 | "color": { 130 | "mode": "palette-classic" 131 | }, 132 | "custom": { 133 | "axisCenteredZero": false, 134 | "axisColorMode": "text", 135 | "axisLabel": "", 136 | "axisPlacement": "auto", 137 | "barAlignment": 0, 138 | "drawStyle": "line", 139 | "fillOpacity": 20, 140 | "gradientMode": "none", 141 | "hideFrom": { 142 | "legend": false, 143 | "tooltip": false, 144 | "viz": false 145 | }, 146 | "lineInterpolation": "smooth", 147 | "lineWidth": 2, 148 | "pointSize": 5, 149 | "scaleDistribution": { 150 | "type": "linear" 151 | }, 152 | "showPoints": "never", 153 | "spanNulls": false, 154 | "stacking": { 155 | "group": "A", 156 | "mode": "none" 157 | }, 158 | "thresholdsStyle": { 159 | "mode": "off" 160 | } 161 | }, 162 | "mappings": [], 163 | "thresholds": { 164 | "mode": "absolute", 165 | "steps": [ 166 | { 167 | "color": "green", 168 | "value": null 169 | }, 170 | { 171 | "color": "yellow", 172 | "value": 70 173 | }, 174 | { 175 | "color": "red", 176 | "value": 85 177 | } 178 | ] 179 | }, 180 | "unit": "percent" 181 | }, 182 | "overrides": [] 183 | }, 184 | "gridPos": { 185 | "h": 8, 186 | "w": 12, 187 | "x": 12, 188 | "y": 0 189 | }, 190 | "id": 2, 191 | "options": { 192 | "legend": { 193 | "calcs": [ 194 | "mean", 195 | "max" 196 | ], 197 | "displayMode": "table", 198 | "placement": "bottom", 199 | "showLegend": true 200 | }, 201 | "tooltip": { 202 | "mode": "single", 203 | "sort": "none" 204 | } 205 | }, 206 | "pluginVersion": "11.4.0", 207 | "targets": [ 208 | { 209 | "datasource": { 210 | "type": "prometheus", 211 | "uid": "prometheus" 212 | }, 213 | "expr": "rate(node_process_cpu_user_seconds_total{service=\"express-api\"}[1m]) * 100", 214 | "legendFormat": "User CPU %", 215 | "refId": "A" 216 | }, 217 | { 218 | "datasource": { 219 | "type": "prometheus", 220 | "uid": "prometheus" 221 | }, 222 | "expr": "rate(node_process_cpu_system_seconds_total{service=\"express-api\"}[1m]) * 100", 223 | "legendFormat": "System CPU %", 224 | "refId": "B" 225 | } 226 | ], 227 | "title": "CPU Usage", 228 | "type": "timeseries" 229 | }, 230 | { 231 | "datasource": { 232 | "type": "prometheus" 233 | }, 234 | "fieldConfig": { 235 | "defaults": { 236 | "mappings": [], 237 | "thresholds": { 238 | "mode": "absolute", 239 | "steps": [ 240 | { 241 | "color": "green", 242 | "value": null 243 | }, 244 | { 245 | "color": "red", 246 | "value": 80 247 | } 248 | ] 249 | } 250 | }, 251 | "overrides": [] 252 | }, 253 | "gridPos": { 254 | "h": 3, 255 | "w": 6, 256 | "x": 0, 257 | "y": 8 258 | }, 259 | "id": 8, 260 | "description": "Shows if Prometheus is successfully collecting metrics from your API endpoint. 1 = UP, 0 = DOWN", 261 | "options": { 262 | "colorMode": "value", 263 | "graphMode": "area", 264 | "justifyMode": "auto", 265 | "orientation": "auto", 266 | "percentChangeColorMode": "standard", 267 | "reduceOptions": { 268 | "calcs": [ 269 | "lastNotNull" 270 | ], 271 | "fields": "", 272 | "values": false 273 | }, 274 | "showPercentChange": false, 275 | "textMode": "auto", 276 | "wideLayout": true 277 | }, 278 | "pluginVersion": "11.4.0", 279 | "targets": [ 280 | { 281 | "datasource": { 282 | "type": "prometheus", 283 | "uid": "prometheus" 284 | }, 285 | "expr": "up{job=\"express-boilerplate\"}", 286 | "legendFormat": "API Status", 287 | "refId": "A" 288 | } 289 | ], 290 | "title": "API Scrape Status", 291 | "type": "stat" 292 | }, 293 | { 294 | "datasource": { 295 | "type": "prometheus", 296 | "uid": "prometheus" 297 | }, 298 | "fieldConfig": { 299 | "defaults": { 300 | "color": { 301 | "mode": "palette-classic" 302 | }, 303 | "custom": { 304 | "axisBorderShow": false, 305 | "axisCenteredZero": false, 306 | "axisColorMode": "text", 307 | "axisLabel": "Memory Usage", 308 | "axisPlacement": "auto", 309 | "barAlignment": 0, 310 | "barWidthFactor": 0.6, 311 | "drawStyle": "line", 312 | "fillOpacity": 10, 313 | "gradientMode": "none", 314 | "hideFrom": { 315 | "legend": false, 316 | "tooltip": false, 317 | "viz": false 318 | }, 319 | "insertNulls": false, 320 | "lineInterpolation": "linear", 321 | "lineWidth": 1, 322 | "pointSize": 5, 323 | "scaleDistribution": { 324 | "type": "linear" 325 | }, 326 | "showPoints": "never", 327 | "spanNulls": false, 328 | "stacking": { 329 | "group": "A", 330 | "mode": "none" 331 | }, 332 | "thresholdsStyle": { 333 | "mode": "line" 334 | } 335 | }, 336 | "mappings": [], 337 | "max": 100, 338 | "min": 0, 339 | "thresholds": { 340 | "mode": "absolute", 341 | "steps": [ 342 | { 343 | "color": "green", 344 | "value": null 345 | }, 346 | { 347 | "color": "yellow", 348 | "value": 75 349 | }, 350 | { 351 | "color": "red", 352 | "value": 90 353 | } 354 | ] 355 | }, 356 | "unit": "percent" 357 | }, 358 | "overrides": [] 359 | }, 360 | "gridPos": { 361 | "h": 8, 362 | "w": 12, 363 | "x": 12, 364 | "y": 8 365 | }, 366 | "id": 4, 367 | "options": { 368 | "legend": { 369 | "calcs": [ 370 | "mean", 371 | "max" 372 | ], 373 | "displayMode": "table", 374 | "placement": "bottom", 375 | "showLegend": true 376 | }, 377 | "tooltip": { 378 | "mode": "single", 379 | "sort": "none" 380 | } 381 | }, 382 | "pluginVersion": "11.4.0", 383 | "targets": [ 384 | { 385 | "datasource": { 386 | "type": "prometheus", 387 | "uid": "prometheus" 388 | }, 389 | "expr": "(node_nodejs_heap_size_used_bytes{service=\"express-api\"} / node_nodejs_heap_size_total_bytes{service=\"express-api\"}) * 100", 390 | "legendFormat": "Heap Usage %", 391 | "refId": "A" 392 | }, 393 | { 394 | "datasource": { 395 | "type": "prometheus", 396 | "uid": "prometheus" 397 | }, 398 | "expr": "node_nodejs_heap_size_used_bytes{service=\"express-api\"} / 1024 / 1024", 399 | "legendFormat": "Heap Used (MB)", 400 | "refId": "B" 401 | } 402 | ], 403 | "title": "Memory Usage", 404 | "type": "timeseries" 405 | }, 406 | { 407 | "datasource": { 408 | "type": "prometheus", 409 | "uid": "prometheus" 410 | }, 411 | "description": "Number of server errors (HTTP 500 status codes) per second. Spikes here indicate problems with your API.", 412 | "fieldConfig": { 413 | "defaults": { 414 | "color": { 415 | "mode": "palette-classic" 416 | }, 417 | "custom": { 418 | "axisBorderShow": false, 419 | "axisCenteredZero": false, 420 | "axisColorMode": "text", 421 | "axisLabel": "", 422 | "axisPlacement": "auto", 423 | "barAlignment": 0, 424 | "drawStyle": "line", 425 | "fillOpacity": 10, 426 | "gradientMode": "none", 427 | "hideFrom": { 428 | "legend": false, 429 | "tooltip": false, 430 | "viz": false 431 | }, 432 | "lineInterpolation": "linear", 433 | "lineWidth": 1, 434 | "pointSize": 5, 435 | "scaleDistribution": { 436 | "type": "linear" 437 | }, 438 | "showPoints": "never", 439 | "spanNulls": false, 440 | "stacking": { 441 | "group": "A", 442 | "mode": "none" 443 | }, 444 | "thresholdsStyle": { 445 | "mode": "off" 446 | } 447 | }, 448 | "mappings": [ 449 | { 450 | "options": { 451 | "null": { 452 | "color": "green", 453 | "index": 0, 454 | "text": "No Errors" 455 | } 456 | }, 457 | "type": "value" 458 | } 459 | ], 460 | "noValue": "No Errors", 461 | "thresholds": { 462 | "mode": "absolute", 463 | "steps": [ 464 | { 465 | "color": "green", 466 | "value": null 467 | }, 468 | { 469 | "color": "red", 470 | "value": 1 471 | } 472 | ] 473 | } 474 | }, 475 | "overrides": [] 476 | }, 477 | "gridPos": { 478 | "h": 8, 479 | "w": 12, 480 | "x": 0, 481 | "y": 11 482 | }, 483 | "id": 3, 484 | "options": { 485 | "legend": { 486 | "calcs": [ 487 | "mean", 488 | "max" 489 | ], 490 | "displayMode": "table", 491 | "placement": "bottom", 492 | "showLegend": true 493 | }, 494 | "tooltip": { 495 | "mode": "single", 496 | "sort": "none" 497 | } 498 | }, 499 | "pluginVersion": "11.4.0", 500 | "targets": [ 501 | { 502 | "datasource": { 503 | "type": "prometheus", 504 | "uid": "prometheus" 505 | }, 506 | "expr": "sum(rate(http_errors_total[5m])) by (status_code)", 507 | "legendFormat": "{{status_code}}", 508 | "refId": "A" 509 | } 510 | ], 511 | "title": "Server Error Rate", 512 | "type": "timeseries" 513 | }, 514 | { 515 | "datasource": { 516 | "type": "prometheus", 517 | "uid": "prometheus" 518 | }, 519 | "description": "Distribution of HTTP response codes. 2xx=success, 4xx=client errors, 5xx=server errors", 520 | "fieldConfig": { 521 | "defaults": { 522 | "color": { 523 | "mode": "palette-classic" 524 | }, 525 | "custom": { 526 | "axisBorderShow": false, 527 | "axisCenteredZero": false, 528 | "axisColorMode": "text", 529 | "axisLabel": "", 530 | "axisPlacement": "auto", 531 | "barAlignment": 0, 532 | "barWidthFactor": 0.6, 533 | "drawStyle": "line", 534 | "fillOpacity": 10, 535 | "gradientMode": "none", 536 | "hideFrom": { 537 | "legend": false, 538 | "tooltip": false, 539 | "viz": false 540 | }, 541 | "insertNulls": false, 542 | "lineInterpolation": "linear", 543 | "lineWidth": 1, 544 | "pointSize": 5, 545 | "scaleDistribution": { 546 | "type": "linear" 547 | }, 548 | "showPoints": "never", 549 | "spanNulls": false, 550 | "stacking": { 551 | "group": "A", 552 | "mode": "none" 553 | }, 554 | "thresholdsStyle": { 555 | "mode": "off" 556 | } 557 | }, 558 | "mappings": [], 559 | "thresholds": { 560 | "mode": "absolute", 561 | "steps": [ 562 | { 563 | "color": "green", 564 | "value": null 565 | }, 566 | { 567 | "color": "red", 568 | "value": 80 569 | } 570 | ] 571 | } 572 | }, 573 | "overrides": [] 574 | }, 575 | "gridPos": { 576 | "h": 8, 577 | "w": 12, 578 | "x": 12, 579 | "y": 16 580 | }, 581 | "id": 6, 582 | "options": { 583 | "legend": { 584 | "calcs": [ 585 | "mean", 586 | "max" 587 | ], 588 | "displayMode": "table", 589 | "placement": "bottom", 590 | "showLegend": true 591 | }, 592 | "tooltip": { 593 | "mode": "single", 594 | "sort": "none" 595 | } 596 | }, 597 | "pluginVersion": "11.4.0", 598 | "targets": [ 599 | { 600 | "datasource": { 601 | "type": "prometheus", 602 | "uid": "prometheus" 603 | }, 604 | "expr": "sum by (status) (rate(http_requests_total[5m]))", 605 | "legendFormat": "{{status}}", 606 | "refId": "A" 607 | } 608 | ], 609 | "title": "Response Status Codes", 610 | "type": "timeseries" 611 | }, 612 | { 613 | "datasource": { 614 | "type": "prometheus", 615 | "uid": "prometheus" 616 | }, 617 | "description": "Shows the Node.js event loop lag in seconds. High values indicate potential performance issues.", 618 | "fieldConfig": { 619 | "defaults": { 620 | "color": { 621 | "mode": "palette-classic" 622 | }, 623 | "custom": { 624 | "axisCenteredZero": false, 625 | "axisColorMode": "text", 626 | "axisLabel": "", 627 | "axisPlacement": "auto", 628 | "barAlignment": 0, 629 | "drawStyle": "line", 630 | "fillOpacity": 20, 631 | "gradientMode": "none", 632 | "hideFrom": { 633 | "legend": false, 634 | "tooltip": false, 635 | "viz": false 636 | }, 637 | "lineInterpolation": "smooth", 638 | "lineWidth": 2, 639 | "pointSize": 5, 640 | "scaleDistribution": { 641 | "type": "linear" 642 | }, 643 | "showPoints": "never", 644 | "spanNulls": false, 645 | "stacking": { 646 | "group": "A", 647 | "mode": "none" 648 | }, 649 | "thresholdsStyle": { 650 | "mode": "line" 651 | } 652 | }, 653 | "mappings": [], 654 | "thresholds": { 655 | "mode": "absolute", 656 | "steps": [ 657 | { 658 | "color": "green", 659 | "value": null 660 | }, 661 | { 662 | "color": "yellow", 663 | "value": 0.1 664 | }, 665 | { 666 | "color": "red", 667 | "value": 0.3 668 | } 669 | ] 670 | }, 671 | "unit": "s" 672 | }, 673 | "overrides": [] 674 | }, 675 | "gridPos": { 676 | "h": 8, 677 | "w": 12, 678 | "x": 0, 679 | "y": 8 680 | }, 681 | "id": 12, 682 | "options": { 683 | "legend": { 684 | "calcs": [ 685 | "mean", 686 | "max" 687 | ], 688 | "displayMode": "table", 689 | "placement": "bottom", 690 | "showLegend": true 691 | }, 692 | "tooltip": { 693 | "mode": "single", 694 | "sort": "none" 695 | } 696 | }, 697 | "targets": [ 698 | { 699 | "datasource": { 700 | "type": "prometheus", 701 | "uid": "prometheus" 702 | }, 703 | "expr": "node_nodejs_eventloop_lag_seconds{service=\"express-api\"}", 704 | "legendFormat": "Event Loop Lag", 705 | "refId": "A" 706 | } 707 | ], 708 | "title": "Event Loop Lag", 709 | "type": "timeseries" 710 | }, 711 | { 712 | "datasource": { 713 | "type": "prometheus", 714 | "uid": "prometheus" 715 | }, 716 | "description": "Shows p50, p90, p95, p99 request durations to identify performance issues", 717 | "fieldConfig": { 718 | "defaults": { 719 | "color": { 720 | "mode": "palette-classic" 721 | }, 722 | "custom": { 723 | "axisCenteredZero": false, 724 | "axisColorMode": "text", 725 | "axisLabel": "", 726 | "axisPlacement": "auto", 727 | "barAlignment": 0, 728 | "drawStyle": "line", 729 | "fillOpacity": 20, 730 | "gradientMode": "none", 731 | "hideFrom": { 732 | "legend": false, 733 | "tooltip": false, 734 | "viz": false 735 | }, 736 | "lineInterpolation": "smooth", 737 | "lineWidth": 2, 738 | "pointSize": 5, 739 | "scaleDistribution": { 740 | "type": "linear" 741 | }, 742 | "showPoints": "never", 743 | "spanNulls": false, 744 | "stacking": { 745 | "group": "A", 746 | "mode": "none" 747 | }, 748 | "thresholdsStyle": { 749 | "mode": "line" 750 | } 751 | }, 752 | "mappings": [], 753 | "thresholds": { 754 | "mode": "absolute", 755 | "steps": [ 756 | { 757 | "color": "green", 758 | "value": null 759 | }, 760 | { 761 | "color": "yellow", 762 | "value": 0.5 763 | }, 764 | { 765 | "color": "red", 766 | "value": 1 767 | } 768 | ] 769 | }, 770 | "unit": "s" 771 | }, 772 | "overrides": [] 773 | }, 774 | "gridPos": { 775 | "h": 8, 776 | "w": 12, 777 | "x": 0, 778 | "y": 35 779 | }, 780 | "id": 13, 781 | "options": { 782 | "legend": { 783 | "calcs": ["mean", "max"], 784 | "displayMode": "table", 785 | "placement": "bottom", 786 | "showLegend": true 787 | }, 788 | "tooltip": { 789 | "mode": "single", 790 | "sort": "none" 791 | } 792 | }, 793 | "targets": [ 794 | { 795 | "datasource": { 796 | "type": "prometheus", 797 | "uid": "prometheus" 798 | }, 799 | "expr": "histogram_quantile(0.50, rate(http_request_duration_seconds_bucket[5m]))", 800 | "legendFormat": "p50", 801 | "refId": "A" 802 | }, 803 | { 804 | "datasource": { 805 | "type": "prometheus", 806 | "uid": "prometheus" 807 | }, 808 | "expr": "histogram_quantile(0.90, rate(http_request_duration_seconds_bucket[5m]))", 809 | "legendFormat": "p90", 810 | "refId": "B" 811 | }, 812 | { 813 | "datasource": { 814 | "type": "prometheus", 815 | "uid": "prometheus" 816 | }, 817 | "expr": "histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))", 818 | "legendFormat": "p95", 819 | "refId": "C" 820 | }, 821 | { 822 | "datasource": { 823 | "type": "prometheus", 824 | "uid": "prometheus" 825 | }, 826 | "expr": "histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m]))", 827 | "legendFormat": "p99", 828 | "refId": "D" 829 | } 830 | ], 831 | "title": "Request Duration Percentiles", 832 | "type": "timeseries" 833 | }, 834 | { 835 | "datasource": { 836 | "type": "prometheus", 837 | "uid": "prometheus" 838 | }, 839 | "description": "Distribution of HTTP errors by status code and endpoint. Shows which routes are generating the most errors.", 840 | "fieldConfig": { 841 | "defaults": { 842 | "color": { 843 | "mode": "palette-classic" 844 | }, 845 | "custom": { 846 | "axisCenteredZero": false, 847 | "axisColorMode": "text", 848 | "axisLabel": "", 849 | "axisPlacement": "auto", 850 | "drawStyle": "bars", 851 | "fillOpacity": 80, 852 | "gradientMode": "none", 853 | "hideFrom": { 854 | "legend": false, 855 | "tooltip": false, 856 | "viz": false 857 | }, 858 | "lineWidth": 1, 859 | "scaleDistribution": { 860 | "type": "linear" 861 | }, 862 | "showPoints": "never", 863 | "spanNulls": false 864 | }, 865 | "mappings": [ 866 | { 867 | "options": { 868 | "null": { 869 | "color": "green", 870 | "index": 0, 871 | "text": "No Errors" 872 | } 873 | }, 874 | "type": "value" 875 | } 876 | ], 877 | "noValue": "No Errors", 878 | "thresholds": { 879 | "mode": "absolute", 880 | "steps": [ 881 | { 882 | "color": "green", 883 | "value": null 884 | }, 885 | { 886 | "color": "yellow", 887 | "value": 0.1 888 | }, 889 | { 890 | "color": "red", 891 | "value": 1 892 | } 893 | ] 894 | }, 895 | "unit": "short" 896 | }, 897 | "overrides": [] 898 | }, 899 | "gridPos": { 900 | "h": 8, 901 | "w": 12, 902 | "x": 12, 903 | "y": 43 904 | }, 905 | "id": 16, 906 | "options": { 907 | "legend": { 908 | "calcs": [ 909 | "mean", 910 | "max" 911 | ], 912 | "displayMode": "table", 913 | "placement": "bottom", 914 | "showLegend": true 915 | }, 916 | "tooltip": { 917 | "mode": "single", 918 | "sort": "desc" 919 | } 920 | }, 921 | "targets": [ 922 | { 923 | "datasource": { 924 | "type": "prometheus", 925 | "uid": "prometheus" 926 | }, 927 | "expr": "sum(rate(http_errors_total[5m])) by (route, status_code)", 928 | "legendFormat": "{{status_code}} - {{route}}", 929 | "refId": "A" 930 | } 931 | ], 932 | "title": "HTTP Errors by Route", 933 | "type": "timeseries" 934 | }, 935 | { 936 | "datasource": { 937 | "type": "prometheus", 938 | "uid": "prometheus" 939 | }, 940 | "description": "Raw Node.js process metrics including memory usage and CPU time", 941 | "fieldConfig": { 942 | "defaults": { 943 | "color": { 944 | "mode": "palette-classic" 945 | }, 946 | "mappings": [], 947 | "thresholds": { 948 | "mode": "absolute", 949 | "steps": [ 950 | { 951 | "color": "green", 952 | "value": null 953 | }, 954 | { 955 | "color": "red", 956 | "value": 80 957 | } 958 | ] 959 | }, 960 | "unit": "bytes" 961 | } 962 | }, 963 | "gridPos": { 964 | "h": 8, 965 | "w": 12, 966 | "x": 0, 967 | "y": 0 968 | }, 969 | "id": 20, 970 | "options": { 971 | "legend": { 972 | "calcs": ["mean", "max"], 973 | "displayMode": "table", 974 | "placement": "bottom" 975 | } 976 | }, 977 | "targets": [ 978 | { 979 | "expr": "node_process_stats{service=\"express-api\"}", 980 | "legendFormat": "{{stat}}", 981 | "refId": "A" 982 | } 983 | ], 984 | "title": "Node.js Process Stats", 985 | "type": "timeseries" 986 | }, 987 | { 988 | "datasource": { 989 | "type": "prometheus", 990 | "uid": "prometheus" 991 | }, 992 | "description": "Garbage Collection statistics", 993 | "fieldConfig": { 994 | "defaults": { 995 | "color": { 996 | "mode": "palette-classic" 997 | }, 998 | "unit": "s" 999 | } 1000 | }, 1001 | "gridPos": { 1002 | "h": 8, 1003 | "w": 12, 1004 | "x": 12, 1005 | "y": 0 1006 | }, 1007 | "id": 21, 1008 | "targets": [ 1009 | { 1010 | "expr": "rate(node_gc_duration_seconds_sum[5m])", 1011 | "legendFormat": "{{gc_type}}", 1012 | "refId": "A" 1013 | }, 1014 | { 1015 | "expr": "rate(node_gc_duration_seconds_count[5m])", 1016 | "legendFormat": "{{gc_type}} (count)", 1017 | "refId": "B" 1018 | } 1019 | ], 1020 | "title": "Garbage Collection Stats", 1021 | "type": "timeseries", 1022 | "options": { 1023 | "tooltip": { 1024 | "mode": "multi", 1025 | "sort": "desc" 1026 | } 1027 | } 1028 | } 1029 | ], 1030 | "preload": false, 1031 | "refresh": "5s", 1032 | "schemaVersion": 40, 1033 | "tags": [], 1034 | "templating": { 1035 | "list": [] 1036 | }, 1037 | "time": { 1038 | "from": "now-15m", 1039 | "to": "now" 1040 | }, 1041 | "timepicker": {}, 1042 | "timezone": "", 1043 | "title": "Express API Dashboard", 1044 | "uid": "express-dashboard", 1045 | "version": 10, 1046 | "weekStart": "" 1047 | } -------------------------------------------------------------------------------- /data/grafana/provisioning/dashboards/default.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | providers: 4 | - name: 'Default' 5 | orgId: 1 6 | folder: '' 7 | folderUid: '' 8 | type: file 9 | disableDeletion: false 10 | updateIntervalSeconds: 10 11 | allowUiUpdates: true 12 | options: 13 | path: /etc/grafana/dashboards 14 | foldersFromFilesStructure: true -------------------------------------------------------------------------------- /data/grafana/provisioning/datasources/prometheus.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | datasources: 4 | - name: Prometheus 5 | type: prometheus 6 | access: proxy 7 | url: http://prometheus:9090 8 | isDefault: true 9 | editable: true 10 | jsonData: 11 | timeInterval: "5s" 12 | queryTimeout: "30s" 13 | httpMethod: "POST" 14 | secureJsonData: 15 | httpHeaderValue1: "" 16 | version: 1 17 | uid: prometheus -------------------------------------------------------------------------------- /data/prometheus/prometheus.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 15s 3 | evaluation_interval: 15s 4 | 5 | # Alertmanager configuration 6 | alerting: 7 | alertmanagers: 8 | - static_configs: 9 | - targets: 10 | - alertmanager:9093 11 | 12 | # Load rules once and periodically evaluate them 13 | rule_files: 14 | - "rules/recording_rules.yml" 15 | - "rules/alerting_rules.yml" 16 | 17 | scrape_configs: 18 | - job_name: 'express-api' 19 | metrics_path: '/monitoring/metrics' 20 | static_configs: 21 | - targets: ['api:4300'] 22 | scrape_interval: 5s 23 | 24 | - job_name: "prometheus" 25 | static_configs: 26 | - targets: ["localhost:9090"] 27 | 28 | # Node Exporter for system metrics 29 | - job_name: "node" 30 | static_configs: 31 | - targets: ["node-exporter:9100"] 32 | 33 | - job_name: 'alertmanager' 34 | static_configs: 35 | - targets: ['alertmanager:9093'] 36 | -------------------------------------------------------------------------------- /data/rules/alerting_rules.yml: -------------------------------------------------------------------------------- 1 | groups: 2 | - name: cpu_alerts 3 | rules: 4 | - alert: HighCPUUsage 5 | expr: rate(process_cpu_user_seconds_total[1m]) * 100 > 80 6 | for: 5m 7 | labels: 8 | severity: warning 9 | annotations: 10 | summary: "High CPU Usage detected" 11 | description: "CPU Usage is above 80% for 5 minutes" 12 | 13 | - name: memory_alerts 14 | rules: 15 | - alert: HighMemoryUsage 16 | expr: (nodejs_heap_size_used_bytes / nodejs_heap_size_total_bytes) * 100 > 85 17 | for: 5m 18 | labels: 19 | severity: warning 20 | annotations: 21 | summary: "High Memory Usage detected" 22 | description: "Memory Usage is above 85% for 5 minutes" 23 | 24 | - name: error_alerts 25 | rules: 26 | - alert: HighErrorRate 27 | expr: rate(http_requests_total{status=~"5.*"}[5m]) > 1 28 | for: 2m 29 | labels: 30 | severity: critical 31 | annotations: 32 | summary: "High Error Rate detected" 33 | description: "5xx error rate is above 1 req/sec for 2 minutes" 34 | 35 | - name: express_alerts 36 | rules: 37 | - alert: HighRequestLatency 38 | expr: http_request_duration_seconds > 1 39 | for: 1m 40 | labels: 41 | severity: warning 42 | annotations: 43 | summary: "High request latency on {{ $labels.instance }}" 44 | description: "Request latency is above 1s (current value: {{ $value }}s)" -------------------------------------------------------------------------------- /data/rules/recording_rules.yml: -------------------------------------------------------------------------------- 1 | groups: 2 | - name: express_rules 3 | rules: 4 | - record: job:http_requests_total:rate5m 5 | expr: rate(http_requests_total[5m]) 6 | 7 | - record: job:http_request_duration_seconds:p95 8 | expr: histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket[5m])) by (le)) 9 | 10 | - record: job:errors_total:rate5m 11 | expr: rate(http_requests_total{status=~"5.*"}[5m]) 12 | 13 | - record: job:http_errors_total:rate5m 14 | expr: sum(rate(http_errors_total[5m])) by (status_code, route) 15 | 16 | - name: node_resources 17 | rules: 18 | - record: job:node_cpu_usage:rate1m 19 | expr: rate(node_process_cpu_seconds_total{service="express-api"}[1m]) * 100 20 | 21 | - record: job:node_memory_usage:percent 22 | expr: (node_nodejs_heap_size_used_bytes{service="express-api"} / node_nodejs_heap_size_total_bytes{service="express-api"}) * 100 -------------------------------------------------------------------------------- /docker-compose.dev.yml: -------------------------------------------------------------------------------- 1 | services: 2 | api: 3 | build: 4 | context: . 5 | dockerfile: Dockerfile.dev 6 | volumes: 7 | - ./src:/app/src:delegated 8 | - ./prisma:/app/prisma:delegated 9 | - ./package.json:/app/package.json:delegated 10 | - ./tsconfig.json:/app/tsconfig.json:delegated 11 | - ./scripts:/app/scripts:delegated 12 | - ./tsconfig.scripts.json:/app/tsconfig.scripts.json:delegated 13 | - ./nodemon.json:/app/nodemon.json:delegated 14 | - api_node_modules:/app/node_modules 15 | - api_dist:/app/dist 16 | env_file: .env 17 | ports: 18 | - "4300:4300" 19 | environment: 20 | - NODE_ENV=development 21 | - PORT=4300 22 | - MYSQL_DATABASE_URL=mysql://express-boilerplate:express-boilerplate@db:3306/express-boilerplate 23 | - RUN_SEEDS=true 24 | - PROMETHEUS_USER=${PROMETHEUS_USER:-admin} 25 | - PROMETHEUS_PASSWORD=${PROMETHEUS_PASSWORD:-admin} 26 | - CHOKIDAR_USEPOLLING=true 27 | - CHOKIDAR_INTERVAL=1000 28 | depends_on: 29 | db: 30 | condition: service_healthy 31 | prometheus: 32 | condition: service_started 33 | grafana: 34 | condition: service_started 35 | command: > 36 | sh -c " 37 | mkdir -p /app/node_modules/.prisma && 38 | npx prisma generate && 39 | npx prisma migrate deploy && 40 | if [ \"$$RUN_SEEDS\" = \"true\" ]; then npm run seed:dev; fi && 41 | npx nodemon 42 | " 43 | networks: 44 | - app-network 45 | healthcheck: 46 | test: ["CMD", "wget", "--spider", "http://localhost:4300/monitoring/health"] 47 | interval: 10s 48 | timeout: 5s 49 | retries: 3 50 | restart: unless-stopped 51 | 52 | db: 53 | image: mysql:8.0 54 | env_file: .env 55 | ports: 56 | - "3306:3306" 57 | environment: 58 | MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-rootpassword} 59 | MYSQL_DATABASE: ${MYSQL_DATABASE:-express-boilerplate} 60 | MYSQL_USER: ${MYSQL_USER:-express-boilerplate} 61 | MYSQL_PASSWORD: ${MYSQL_PASSWORD:-express-boilerplate} 62 | volumes: 63 | - mysql_data:/var/lib/mysql:delegated 64 | healthcheck: 65 | test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] 66 | interval: 10s 67 | timeout: 5s 68 | retries: 5 69 | networks: 70 | - app-network 71 | 72 | prometheus: 73 | image: prom/prometheus:latest 74 | ports: 75 | - "9090:9090" 76 | volumes: 77 | - ./data/prometheus:/etc/prometheus:ro 78 | - prometheus_data:/prometheus 79 | command: 80 | - "--config.file=/etc/prometheus/prometheus.yml" 81 | - "--storage.tsdb.path=/prometheus" 82 | - "--web.enable-lifecycle" 83 | - "--web.enable-admin-api" 84 | networks: 85 | - app-network 86 | healthcheck: 87 | test: ["CMD", "wget", "--spider", "http://localhost:9090/-/healthy"] 88 | interval: 10s 89 | timeout: 5s 90 | retries: 3 91 | deploy: 92 | resources: 93 | limits: 94 | memory: 512M 95 | reservations: 96 | memory: 256M 97 | restart: unless-stopped 98 | 99 | grafana: 100 | image: grafana/grafana:latest 101 | ports: 102 | - "3000:3000" 103 | environment: 104 | - GF_SECURITY_ADMIN_PASSWORD=admin 105 | - GF_INSTALL_PLUGINS=grafana-clock-panel 106 | - GF_PLUGINS_ALLOW_LOADING_UNSIGNED_PLUGINS=grafana-simple-json-datasource 107 | - GF_DASHBOARDS_MIN_REFRESH_INTERVAL=1s 108 | - GF_AUTH_ANONYMOUS_ENABLED=true 109 | - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin 110 | - GF_LOG_LEVEL=debug 111 | - GF_LOG_MODE=console 112 | volumes: 113 | - grafana_data:/var/lib/grafana 114 | - ./data/grafana/provisioning:/etc/grafana/provisioning 115 | - ./data/grafana/dashboards:/etc/grafana/dashboards 116 | depends_on: 117 | - prometheus 118 | networks: 119 | - app-network 120 | user: "472" 121 | healthcheck: 122 | test: ["CMD", "wget", "--spider", "http://localhost:3000/api/health"] 123 | interval: 10s 124 | timeout: 5s 125 | retries: 3 126 | deploy: 127 | resources: 128 | limits: 129 | memory: 256M 130 | restart: unless-stopped 131 | 132 | node-exporter: 133 | image: prom/node-exporter:latest 134 | container_name: node-exporter 135 | restart: unless-stopped 136 | volumes: 137 | - /proc:/host/proc:ro 138 | - /sys:/host/sys:ro 139 | - /:/rootfs:ro 140 | command: 141 | - '--path.procfs=/host/proc' 142 | - '--path.rootfs=/rootfs' 143 | - '--path.sysfs=/host/sys' 144 | - '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)' 145 | networks: 146 | - app-network 147 | 148 | alertmanager: 149 | image: prom/alertmanager:latest 150 | ports: 151 | - "9093:9093" 152 | volumes: 153 | - ./data/alertmanager:/etc/alertmanager 154 | command: 155 | - '--config.file=/etc/alertmanager/alertmanager.yml' 156 | - '--storage.path=/alertmanager' 157 | networks: 158 | - app-network 159 | restart: unless-stopped 160 | 161 | volumes: 162 | mysql_data: 163 | driver: local 164 | api_node_modules: 165 | driver: local 166 | api_dist: 167 | driver: local 168 | grafana_data: 169 | driver: local 170 | prometheus_data: 171 | driver: local 172 | 173 | networks: 174 | app-network: 175 | driver: bridge -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | api: 3 | build: . 4 | env_file: .env 5 | ports: 6 | - "4300:4300" 7 | environment: 8 | - NODE_ENV=production 9 | - PORT=4300 10 | - MYSQL_DATABASE_URL=mysql://express-boilerplate:express-boilerplate@db:3306/express-boilerplate 11 | - JWT_SECRET=${JWT_SECRET} 12 | - REFRESH_TOKEN_SECRET=${REFRESH_TOKEN_SECRET} 13 | - FRONTEND_URL=${FRONTEND_URL} 14 | depends_on: 15 | db: 16 | condition: service_healthy 17 | restart: unless-stopped 18 | healthcheck: 19 | test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:4300/health"] 20 | interval: 30s 21 | timeout: 3s 22 | retries: 3 23 | networks: 24 | - app-network 25 | volumes: 26 | - ./logs:/app/logs 27 | tmpfs: 28 | - /tmp 29 | - /run 30 | security_opt: 31 | - no-new-privileges:true 32 | cap_drop: 33 | - ALL 34 | cap_add: 35 | - NET_BIND_SERVICE 36 | deploy: 37 | resources: 38 | limits: 39 | cpus: '1' 40 | memory: 1G 41 | reservations: 42 | cpus: '0.25' 43 | memory: 512M 44 | logging: 45 | driver: "json-file" 46 | options: 47 | max-size: "10m" 48 | max-file: "3" 49 | 50 | db: 51 | image: mysql:8.0 52 | env_file: .env 53 | ports: 54 | - "3306:3306" 55 | environment: 56 | MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-rootpassword} 57 | MYSQL_DATABASE: ${MYSQL_DATABASE:-express-boilerplate} 58 | MYSQL_USER: ${MYSQL_USER:-express-boilerplate} 59 | MYSQL_PASSWORD: ${MYSQL_PASSWORD:-express-boilerplate} 60 | volumes: 61 | - mysql_data:/var/lib/mysql:delegated 62 | healthcheck: 63 | test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] 64 | interval: 10s 65 | timeout: 5s 66 | retries: 5 67 | cap_drop: 68 | - ALL 69 | cap_add: 70 | - CHOWN 71 | - SETGID 72 | - SETUID 73 | - DAC_OVERRIDE 74 | - NET_BIND_SERVICE 75 | security_opt: 76 | - seccomp=unconfined 77 | user: mysql 78 | 79 | prometheus: 80 | image: prom/prometheus:latest 81 | ports: 82 | - "9090:9090" 83 | volumes: 84 | - ./data/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml 85 | - ./data/rules:/etc/prometheus/rules 86 | command: 87 | - "--config.file=/etc/prometheus/prometheus.yml" 88 | networks: 89 | - app-network 90 | user: "nobody" 91 | 92 | grafana: 93 | image: grafana/grafana:latest 94 | ports: 95 | - "3000:3000" 96 | environment: 97 | - GF_SECURITY_ADMIN_PASSWORD=admin 98 | - GF_INSTALL_PLUGINS=grafana-clock-panel 99 | - GF_PLUGINS_ALLOW_LOADING_UNSIGNED_PLUGINS=grafana-simple-json-datasource 100 | - GF_DASHBOARDS_MIN_REFRESH_INTERVAL=1s 101 | volumes: 102 | - grafana_data:/var/lib/grafana 103 | - ./data/grafana/provisioning:/etc/grafana/provisioning 104 | - ./data/grafana/dashboards:/etc/grafana/dashboards 105 | depends_on: 106 | prometheus: 107 | condition: service_started 108 | networks: 109 | - app-network 110 | healthcheck: 111 | test: ["CMD-SHELL", "wget -q --spider http://localhost:3000/api/health || exit 1"] 112 | interval: 10s 113 | timeout: 5s 114 | retries: 5 115 | user: "472" 116 | 117 | node-exporter: 118 | image: prom/node-exporter:latest 119 | container_name: node-exporter 120 | restart: unless-stopped 121 | volumes: 122 | - /proc:/host/proc:ro 123 | - /sys:/host/sys:ro 124 | - /:/rootfs:ro 125 | command: 126 | - '--path.procfs=/host/proc' 127 | - '--path.rootfs=/rootfs' 128 | - '--path.sysfs=/host/sys' 129 | - '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)' 130 | networks: 131 | - app-network 132 | 133 | alertmanager: 134 | image: prom/alertmanager:latest 135 | ports: 136 | - "9093:9093" 137 | volumes: 138 | - ./data/alertmanager:/etc/alertmanager 139 | command: 140 | - '--config.file=/etc/alertmanager/alertmanager.yml' 141 | - '--storage.path=/alertmanager' 142 | networks: 143 | - app-network 144 | restart: unless-stopped 145 | 146 | volumes: 147 | api_logs: 148 | driver: local 149 | driver_opts: 150 | type: none 151 | o: bind 152 | device: ./logs 153 | mysql_data: 154 | driver: local 155 | grafana_data: 156 | driver: local 157 | 158 | networks: 159 | app-network: 160 | driver: bridge 161 | ipam: 162 | driver: default 163 | config: 164 | - subnet: 172.20.0.0/16 165 | driver_opts: 166 | encrypt: "true" -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "jest"; 2 | 3 | const config: Config = { 4 | preset: "ts-jest", 5 | testEnvironment: "node", 6 | roots: ["/src"], 7 | testMatch: ["**/__tests__/**/*.test.ts"], 8 | moduleNameMapper: { 9 | "^@/(.*)$": "/src/$1", 10 | }, 11 | setupFiles: ["dotenv/config"], 12 | coverageDirectory: "coverage", 13 | collectCoverageFrom: [ 14 | "src/**/*.ts", 15 | "!src/**/*.d.ts", 16 | "!src/__tests__/**", 17 | "!src/index.ts", 18 | ], 19 | coverageThreshold: { 20 | global: { 21 | branches: 80, 22 | functions: 80, 23 | lines: 80, 24 | statements: 80 25 | } 26 | } 27 | }; 28 | 29 | export default config; 30 | -------------------------------------------------------------------------------- /jest.e2e.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "jest"; 2 | import baseConfig from "./jest.config"; 3 | 4 | const config: Config = { 5 | ...baseConfig, 6 | testMatch: ["**/__tests__/**/*.e2e.test.ts"], 7 | setupFilesAfterEnv: ["/src/__tests__/setup.e2e.ts"], 8 | }; 9 | 10 | export default config; 11 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": ".ts,.js", 4 | "ignore": ["src/**/*.spec.ts", "src/**/*.test.ts"], 5 | "exec": "node -r ts-node/register -r tsconfig-paths/register src/index.ts", 6 | "legacyWatch": true, 7 | "delay": "1000", 8 | "polling": true 9 | } 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-boilerplate", 3 | "version": "1.0.0", 4 | "main": "index.ts", 5 | "license": "MIT", 6 | "scripts": { 7 | "start": "node --expose-gc --trace-gc --trace-gc-ignore-scavenger --node-options='--require=perf_hooks' dist/index.js", 8 | "dev": "npx nodemon", 9 | "dev:clean": "rimraf dist && tsx watch src/index.ts", 10 | "build": "tsc -p . && tsc-alias", 11 | "docker:dev": "docker compose -f docker-compose.dev.yml up", 12 | "docker:dev:build": "docker compose -f docker-compose.dev.yml up --build", 13 | "docker:dev:down": "docker compose -f docker-compose.dev.yml down -v", 14 | "seed:dev": "ts-node -P tsconfig.scripts.json scripts/dev.seed.ts", 15 | "seed:prod": "ts-node -P tsconfig.scripts.json scripts/prod.seed.ts", 16 | "migrate:dev": "npx prisma migrate dev", 17 | "migrate": "npx prisma migrate deploy", 18 | "generate": "npx prisma generate", 19 | "studio": "npx prisma studio", 20 | "test": "vitest", 21 | "test:watch": "jest --watch", 22 | "test:coverage": "vitest run --coverage", 23 | "test:e2e": "jest --config jest.e2e.config.ts", 24 | "lint": "eslint . --ext .ts", 25 | "format": "prettier --write \"src/**/*.ts\"" 26 | }, 27 | "_moduleAliases": { 28 | "@": "dist/src" 29 | }, 30 | "devDependencies": { 31 | "@types/bcrypt": "^5.0.2", 32 | "@types/compression": "^1.7.5", 33 | "@types/cors": "^2.8.17", 34 | "@types/express": "^5.0.0", 35 | "@types/express-rate-limit": "^5.1.3", 36 | "@types/jest": "^29.5.14", 37 | "@types/jsonwebtoken": "^9.0.7", 38 | "@types/node": "^22.10.5", 39 | "@types/nodemailer": "^6.4.17", 40 | "@types/response-time": "^2.3.8", 41 | "@types/supertest": "^6.0.2", 42 | "@types/swagger-jsdoc": "^6.0.4", 43 | "@types/swagger-ui-express": "^4.1.7", 44 | "@types/ws": "^8.5.13", 45 | "@typescript-eslint/eslint-plugin": "^6.0.0", 46 | "@typescript-eslint/parser": "^6.0.0", 47 | "eslint": "^8.0.0", 48 | "jest": "^29.7.0", 49 | "nodemon": "^3.1.9", 50 | "prettier": "^3.0.0", 51 | "prisma": "^6.0.0", 52 | "supertest": "^7.0.0", 53 | "ts-jest": "^29.2.5", 54 | "ts-node": "^10.9.2", 55 | "tsc-alias": "^1.8.10", 56 | "tsconfig-paths": "^4.2.0", 57 | "tsx": "^4.19.2", 58 | "typescript": "^5.7.2", 59 | "vitest": "^1.0.0" 60 | }, 61 | "dependencies": { 62 | "@prisma/client": "^6.0.0", 63 | "bcrypt": "^5.1.1", 64 | "compression": "^1.7.5", 65 | "cors": "^2.8.5", 66 | "dotenv": "^16.4.7", 67 | "express": "^4.21.0", 68 | "express-rate-limit": "^7.5.0", 69 | "express-winston": "^4.2.0", 70 | "helmet": "^8.0.0", 71 | "jsonwebtoken": "^9.0.2", 72 | "module-alias": "^2.2.3", 73 | "nodemailer": "^6.9.16", 74 | "prom-client": "^15.1.3", 75 | "response-time": "^2.3.3", 76 | "swagger-jsdoc": "^6.2.8", 77 | "swagger-ui-express": "^5.0.1", 78 | "uuid": "^11.0.5", 79 | "winston": "^3.17.0", 80 | "winston-daily-rotate-file": "^5.0.0", 81 | "ws": "^8.18.0", 82 | "zod": "^3.24.1" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /prisma/migrations/20250110225528_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE `user` ( 3 | `id` VARCHAR(191) NOT NULL, 4 | `name` VARCHAR(99) NOT NULL, 5 | `email` VARCHAR(99) NOT NULL, 6 | `password` VARCHAR(100) NULL, 7 | `refreshToken` TEXT NULL, 8 | `role` ENUM('ADMIN', 'USER') NOT NULL DEFAULT 'USER', 9 | `emailVerified` DATETIME(3) NULL, 10 | `emailVerificationToken` VARCHAR(100) NULL, 11 | `emailVerificationExpires` DATETIME(3) NULL, 12 | `passwordResetToken` VARCHAR(100) NULL, 13 | `passwordResetExpires` DATETIME(3) NULL, 14 | `image` VARCHAR(191) NULL, 15 | `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), 16 | `updatedAt` DATETIME(3) NOT NULL, 17 | 18 | UNIQUE INDEX `user_email_key`(`email`), 19 | PRIMARY KEY (`id`) 20 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 21 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (e.g., Git) 3 | provider = "mysql" -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | } 4 | 5 | datasource db { 6 | provider = "mysql" 7 | url = env("MYSQL_DATABASE_URL") 8 | } 9 | 10 | model user { 11 | id String @id @default(uuid()) 12 | name String @db.VarChar(99) 13 | email String @unique @db.VarChar(99) 14 | password String? @db.VarChar(100) 15 | refreshToken String? @db.Text 16 | role user_role @default(USER) 17 | emailVerified DateTime? 18 | emailVerificationToken String? @db.VarChar(100) 19 | emailVerificationExpires DateTime? 20 | passwordResetToken String? @db.VarChar(100) 21 | passwordResetExpires DateTime? 22 | image String? 23 | createdAt DateTime @default(now()) 24 | updatedAt DateTime @updatedAt 25 | } 26 | 27 | enum user_role { 28 | ADMIN 29 | USER 30 | } 31 | -------------------------------------------------------------------------------- /requests/auth/auth.rest: -------------------------------------------------------------------------------- 1 | @baseUrl = http://localhost:4300/api 2 | 3 | ### Register 4 | POST {{baseUrl}}/auth/signup 5 | Content-Type: application/json 6 | 7 | { 8 | "name": "Test User", 9 | "email": "test@example.com", 10 | "password": "Password123!" 11 | } 12 | 13 | ### Login First 14 | # @name login 15 | POST {{baseUrl}}/auth/login 16 | Content-Type: application/json 17 | 18 | { 19 | "email": "test@example.com", 20 | "password": "Password123!" 21 | } 22 | 23 | ### Get Users 24 | @token = {{login.response.body.data.accessToken}} 25 | GET {{baseUrl}}/users 26 | Authorization: Bearer {{token}} 27 | 28 | ### Refresh Token 29 | POST {{baseUrl}}/auth/refresh 30 | Content-Type: application/json 31 | 32 | { 33 | "refreshToken": "{{login.response.body.data.refreshToken}}" 34 | } 35 | 36 | ### Logout 37 | POST {{baseUrl}}/auth/logout 38 | Authorization: Bearer {{login.response.body.data.accessToken}} -------------------------------------------------------------------------------- /requests/auth/email-tests.rest: -------------------------------------------------------------------------------- 1 | @baseUrl = http://localhost:4300/api 2 | 3 | ### 1. Sign up a new user 4 | POST {{baseUrl}}/auth/signup 5 | Content-Type: application/json 6 | 7 | { 8 | "email": "test@example.com", 9 | "name": "Test User", 10 | "password": "Password123!" 11 | } 12 | 13 | ### 2. Resend verification email 14 | POST {{baseUrl}}/auth/send-email-verification 15 | Content-Type: application/json 16 | 17 | { 18 | "email": "test@example.com" 19 | } -------------------------------------------------------------------------------- /requests/auth/password-reset.rest: -------------------------------------------------------------------------------- 1 | @baseUrl = http://localhost:4300/api 2 | 3 | ### Request Password Reset 4 | POST {{baseUrl}}/auth/forgot-password 5 | Content-Type: application/json 6 | 7 | { 8 | "email": "test@example.com" 9 | } 10 | 11 | ### Reset Password 12 | POST {{baseUrl}}/auth/reset-password/c42116d5b120249e2a4ad1530caf8eb244bf61087f588d1f62610596f5cfb090 13 | Content-Type: application/json 14 | 15 | { 16 | "password": "NewPassword123!" 17 | } -------------------------------------------------------------------------------- /requests/monitoring/monitoring.rest: -------------------------------------------------------------------------------- 1 | @baseUrl = http://localhost:4300/api 2 | 3 | ### Get Metrics 4 | # @name metrics 5 | GET {{baseUrl}}/monitoring/metrics 6 | Accept: text/plain 7 | 8 | ### Get Health Check 9 | # @name health 10 | GET {{baseUrl}}/monitoring/health 11 | Accept: application/json 12 | 13 | ### Get Readiness Check 14 | # @name readiness 15 | GET {{baseUrl}}/monitoring/readiness 16 | Accept: application/json 17 | 18 | ### Get Liveness Check 19 | # @name liveness 20 | GET {{baseUrl}}/monitoring/liveness 21 | Accept: application/json 22 | 23 | ### Simulate Error (multiple times) 24 | GET {{baseUrl}}/monitoring/simulate-error 25 | Accept: application/json 26 | 27 | ### Test Root Endpoint 28 | # @name root 29 | GET {{baseUrl}}/ 30 | Accept: application/json -------------------------------------------------------------------------------- /requests/users/users.rest: -------------------------------------------------------------------------------- 1 | @baseUrl = http://localhost:4300/api 2 | 3 | ### Login First 4 | # @name login 5 | POST {{baseUrl}}/auth/login 6 | Content-Type: application/json 7 | 8 | { 9 | "email": "test@example.com", 10 | "password": "Password123!" 11 | } 12 | 13 | ### 14 | @token = {{login.response.body.data.accessToken}} 15 | 16 | ### Get All Users (Admin Only) 17 | GET {{baseUrl}}/users 18 | Authorization: Bearer {{token}} 19 | 20 | ### Get User by ID 21 | GET {{baseUrl}}/users/uuid-here 22 | Authorization: Bearer {{token}} 23 | 24 | ### Create User (Admin Only) 25 | POST {{baseUrl}}/users 26 | Authorization: Bearer {{token}} 27 | Content-Type: application/json 28 | 29 | { 30 | "name": "New User", 31 | "email": "new@example.com", 32 | "password": "Password123!" 33 | } 34 | 35 | ### Update User (Admin Only) 36 | PATCH {{baseUrl}}/users/1 37 | Authorization: Bearer {{token}} 38 | Content-Type: application/json 39 | 40 | { 41 | "name": "Updated Name" 42 | } 43 | 44 | ### Delete User (Admin Only) 45 | DELETE {{baseUrl}}/users/1 46 | Authorization: Bearer {{token}} -------------------------------------------------------------------------------- /scripts/dev.seed.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from "dotenv"; 2 | dotenv.config({ path: ".env" }); 3 | 4 | import { PrismaClient } from "@prisma/client"; 5 | import bcrypt from "bcrypt"; 6 | const prisma = new PrismaClient(); 7 | 8 | async function main() { 9 | // Clear existing data 10 | await prisma.user.deleteMany({}); 11 | 12 | // Create development test users 13 | const hashedPassword = await bcrypt.hash("Password123!", 10); 14 | 15 | const users = await Promise.all([ 16 | prisma.user.create({ 17 | data: { 18 | name: "John Doe", 19 | email: "john@example.com", 20 | password: hashedPassword, 21 | role: "ADMIN", 22 | }, 23 | }), 24 | prisma.user.create({ 25 | data: { 26 | name: "Jane Smith", 27 | email: "jane@example.com", 28 | password: hashedPassword, 29 | }, 30 | }), 31 | prisma.user.create({ 32 | data: { 33 | name: "Bob Johnson", 34 | email: "bob@example.com", 35 | password: hashedPassword, 36 | }, 37 | }), 38 | ]); 39 | 40 | console.log("Development seed completed:", users); 41 | } 42 | 43 | main() 44 | .catch((e) => { 45 | console.error("Error seeding development data:", e); 46 | process.exit(1); 47 | }) 48 | .finally(async () => { 49 | await prisma.$disconnect(); 50 | }); 51 | -------------------------------------------------------------------------------- /scripts/prod.seed.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from "dotenv"; 2 | dotenv.config({ path: ".env" }); 3 | 4 | import { PrismaClient } from "@prisma/client"; 5 | import bcrypt from "bcrypt"; 6 | const prisma = new PrismaClient(); 7 | 8 | async function main() { 9 | // In production, we might want to be more careful about seeding 10 | // Only seed if the table is empty 11 | const userCount = await prisma.user.count(); 12 | 13 | if (userCount === 0) { 14 | // Create initial admin user 15 | const hashedPassword = await bcrypt.hash("Password123!", 10); 16 | const adminUser = await prisma.user.create({ 17 | data: { 18 | name: "Admin User", 19 | email: "admin@express-boilerplate.com", 20 | password: hashedPassword, 21 | role: "ADMIN", 22 | }, 23 | }); 24 | 25 | console.log("Production seed completed:", adminUser); 26 | } else { 27 | console.log("Skipping production seed - data already exists"); 28 | } 29 | } 30 | 31 | main() 32 | .catch((e) => { 33 | console.error("Error seeding production data:", e); 34 | process.exit(1); 35 | }) 36 | .finally(async () => { 37 | await prisma.$disconnect(); 38 | }); 39 | -------------------------------------------------------------------------------- /src/@types/express/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Request } from "express"; 2 | 3 | declare global { 4 | namespace Express { 5 | interface Request { 6 | user: { 7 | userId: string; 8 | role: string; 9 | }; 10 | requestId: string; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/@types/global.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | var gc: () => void; 3 | namespace NodeJS { 4 | interface Process { 5 | report: { 6 | getReport: () => any; 7 | writeReport: (filename?: string) => void; 8 | }; 9 | } 10 | } 11 | } 12 | 13 | export {}; -------------------------------------------------------------------------------- /src/__tests__/e2e/auth.e2e.test.ts: -------------------------------------------------------------------------------- 1 | import { testApp } from "../setup.e2e"; 2 | import prisma from "@/config/database"; 3 | import bcrypt from "bcrypt"; 4 | 5 | describe("Auth endpoints", () => { 6 | beforeEach(async () => { 7 | // Clean database before each test 8 | await prisma.$transaction([prisma.user.deleteMany()]); 9 | }); 10 | 11 | describe("POST /api/auth/signup", () => { 12 | it("should create a new user", async () => { 13 | const response = await testApp.post("/api/auth/signup").send({ 14 | email: "test@example.com", 15 | name: "Test User", 16 | password: "Password123!", 17 | }); 18 | 19 | expect(response.status).toBe(200); 20 | expect(response.body.data).toHaveProperty("id"); 21 | }); 22 | }); 23 | 24 | describe("POST /api/auth/login", () => { 25 | beforeEach(async () => { 26 | const hashedPassword = await bcrypt.hash("Password123!", 10); 27 | 28 | // Create test user with all required fields 29 | await prisma.user.create({ 30 | data: { 31 | email: "test@example.com", 32 | name: "Test User", 33 | password: hashedPassword, 34 | role: "USER", 35 | emailVerified: null, 36 | image: null, 37 | refreshToken: null, 38 | }, 39 | }); 40 | 41 | // Wait a bit to ensure user is created 42 | await new Promise((resolve) => setTimeout(resolve, 100)); 43 | }); 44 | 45 | it("should login successfully", async () => { 46 | const response = await testApp.post("/api/auth/login").send({ 47 | email: "test@example.com", 48 | password: "Password123!", 49 | }); 50 | 51 | expect(response.status).toBe(200); 52 | expect(response.body.data).toHaveProperty("accessToken"); 53 | }); 54 | }); 55 | 56 | describe("Email verification", () => { 57 | it("should verify email with valid token", async () => { 58 | // Create user with verification token 59 | const user = await prisma.user.create({ 60 | data: { 61 | email: "test@example.com", 62 | name: "Test User", 63 | password: await bcrypt.hash("Password123!", 10), 64 | emailVerificationToken: "test-token", 65 | emailVerificationExpires: new Date(Date.now() + 24 * 60 * 60 * 1000), 66 | }, 67 | }); 68 | 69 | const response = await testApp 70 | .get("/api/auth/verify-email/test-token") 71 | .expect(200); 72 | 73 | expect(response.body.success).toBe(true); 74 | 75 | // Check user is verified 76 | const verifiedUser = await prisma.user.findUnique({ 77 | where: { id: user.id } 78 | }); 79 | expect(verifiedUser?.emailVerified).toBeTruthy(); 80 | }); 81 | }); 82 | 83 | describe("Password Reset", () => { 84 | beforeEach(async () => { 85 | await prisma.user.deleteMany(); 86 | }); 87 | 88 | it("should send password reset email", async () => { 89 | // Create a user first 90 | const user = await prisma.user.create({ 91 | data: { 92 | email: "test@example.com", 93 | name: "Test User", 94 | password: await bcrypt.hash("Password123!", 10), 95 | emailVerified: new Date(), 96 | }, 97 | }); 98 | 99 | const response = await testApp 100 | .post("/api/auth/forgot-password") 101 | .send({ email: "test@example.com" }) 102 | .expect(200); 103 | 104 | expect(response.body.success).toBe(true); 105 | 106 | // Verify token was created 107 | const updatedUser = await prisma.user.findUnique({ 108 | where: { id: user.id }, 109 | }); 110 | expect(updatedUser?.passwordResetToken).toBeTruthy(); 111 | expect(updatedUser?.passwordResetExpires).toBeTruthy(); 112 | }); 113 | 114 | it("should reset password with valid token", async () => { 115 | // Create user with reset token 116 | const resetToken = "test-reset-token"; 117 | const user = await prisma.user.create({ 118 | data: { 119 | email: "test@example.com", 120 | name: "Test User", 121 | password: await bcrypt.hash("OldPassword123!", 10), 122 | passwordResetToken: resetToken, 123 | passwordResetExpires: new Date(Date.now() + 3600000), // 1 hour 124 | emailVerified: new Date(), 125 | }, 126 | }); 127 | 128 | const response = await testApp 129 | .post(`/api/auth/reset-password/${resetToken}`) 130 | .send({ password: "NewPassword123!" }) 131 | .expect(200); 132 | 133 | expect(response.body.success).toBe(true); 134 | 135 | // Verify password was changed 136 | const updatedUser = await prisma.user.findUnique({ 137 | where: { id: user.id }, 138 | }); 139 | expect(updatedUser?.passwordResetToken).toBeNull(); 140 | expect(updatedUser?.passwordResetExpires).toBeNull(); 141 | 142 | // Verify can login with new password 143 | const loginResponse = await testApp 144 | .post("/api/auth/login") 145 | .send({ 146 | email: "test@example.com", 147 | password: "NewPassword123!", 148 | }) 149 | .expect(200); 150 | 151 | expect(loginResponse.body.data.accessToken).toBeTruthy(); 152 | }); 153 | 154 | it("should not reset password with expired token", async () => { 155 | // Create user with expired reset token 156 | await prisma.user.create({ 157 | data: { 158 | email: "test@example.com", 159 | name: "Test User", 160 | password: await bcrypt.hash("Password123!", 10), 161 | passwordResetToken: "expired-token", 162 | passwordResetExpires: new Date(Date.now() - 3600000), // 1 hour ago 163 | emailVerified: new Date(), 164 | }, 165 | }); 166 | 167 | const response = await testApp 168 | .post("/api/auth/reset-password/expired-token") 169 | .send({ password: "NewPassword123!" }) 170 | .expect(400); 171 | 172 | expect(response.body.success).toBe(false); 173 | expect(response.body.error.code).toBe("ERR_1004"); // INVALID_TOKEN 174 | }); 175 | }); 176 | }); 177 | -------------------------------------------------------------------------------- /src/__tests__/helpers/user.helper.ts: -------------------------------------------------------------------------------- 1 | import prisma from "@/config/database"; 2 | import bcrypt from "bcrypt"; 3 | import { v4 as uuidv4 } from "uuid"; 4 | import { user_role } from "@prisma/client"; 5 | 6 | interface CreateTestUserInput { 7 | email?: string; 8 | name?: string; 9 | password?: string; 10 | role?: user_role; 11 | } 12 | 13 | export const createTestUser = async (data: CreateTestUserInput = {}) => { 14 | const plainPassword = data.password || "Password123!"; 15 | const hashedPassword = await bcrypt.hash(plainPassword, 10); 16 | 17 | return prisma.user.create({ 18 | data: { 19 | email: data.email || `test-${uuidv4()}@example.com`, 20 | name: data.name || "Test User", 21 | password: hashedPassword, 22 | role: data.role || "USER", 23 | }, 24 | }); 25 | }; 26 | -------------------------------------------------------------------------------- /src/__tests__/setup.e2e.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | import request from "supertest"; 3 | import app from "@/app"; 4 | 5 | const prisma = new PrismaClient(); 6 | 7 | beforeAll(async () => { 8 | await prisma.$connect(); 9 | // Clean database at start 10 | await prisma.$transaction([prisma.user.deleteMany()]); 11 | }); 12 | 13 | beforeEach(async () => { 14 | // Clean database before each test 15 | await prisma.$transaction([prisma.user.deleteMany()]); 16 | }); 17 | 18 | afterAll(async () => { 19 | await prisma.$transaction([prisma.user.deleteMany()]); 20 | await prisma.$disconnect(); 21 | }); 22 | 23 | export const testApp = request(app); 24 | export { prisma }; 25 | -------------------------------------------------------------------------------- /src/__tests__/websocket/jest.websocket.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "jest"; 2 | import baseConfig from "../../../jest.config"; 3 | 4 | const config: Config = { 5 | ...baseConfig, 6 | testMatch: ["**/__tests__/websocket/**/*.test.ts"], 7 | }; 8 | 9 | export default config; -------------------------------------------------------------------------------- /src/__tests__/websocket/websocket.test.ts: -------------------------------------------------------------------------------- 1 | import WebSocket from 'ws'; 2 | import app from '@/app'; 3 | import { createServer } from 'http'; 4 | import { WebSocketService } from '@/services/websocket.service'; 5 | 6 | const TEST_PORT = 4400; 7 | 8 | describe('WebSocket Tests', () => { 9 | jest.setTimeout(10000); 10 | 11 | let ws: WebSocket; 12 | let server = createServer(app); 13 | 14 | beforeAll(async () => { 15 | return new Promise((resolve) => { 16 | server.listen(TEST_PORT, () => { 17 | WebSocketService.getInstance(server); 18 | resolve(); 19 | }); 20 | }); 21 | }); 22 | 23 | afterAll(async () => { 24 | return new Promise((resolve) => { 25 | ws?.close(); 26 | (WebSocketService as any).instance = null; 27 | server.close(() => resolve()); 28 | }); 29 | }); 30 | 31 | beforeEach(async () => { 32 | return new Promise((resolve, reject) => { 33 | ws = new WebSocket(`ws://localhost:${TEST_PORT}`); 34 | ws.on('open', () => resolve()); 35 | ws.on('error', reject); 36 | }); 37 | }); 38 | 39 | afterEach(() => { 40 | if (ws?.readyState === WebSocket.OPEN) { 41 | ws.close(); 42 | } 43 | }); 44 | 45 | it('should receive connection confirmation', (done) => { 46 | ws.on('message', (data) => { 47 | const message = JSON.parse(data.toString()); 48 | expect(message.type).toBe('connection'); 49 | expect(message.data).toHaveProperty('clientId'); 50 | done(); 51 | }); 52 | }); 53 | 54 | it('should handle ping-pong', (done) => { 55 | ws.on('message', (data) => { 56 | const message = JSON.parse(data.toString()); 57 | if (message.type === 'pong') { 58 | expect(message.data).toHaveProperty('timestamp'); 59 | done(); 60 | } 61 | }); 62 | 63 | ws.send(JSON.stringify({ type: 'ping' })); 64 | }); 65 | }); -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { ENV } from "@/config/env"; 3 | import userRoutes from "@/routes/user.routes"; 4 | import authRoutes from "@/routes/auth.routes"; 5 | import { errorHandler } from "@/middleware/errorHandler"; 6 | import { setupSecurityHeaders } from "@/middleware/securityHeaders"; 7 | import { apiLimiter } from "@/middleware/rateLimiter"; 8 | import { authLimiter } from "@/middleware/rateLimiter"; 9 | import cors from "cors"; 10 | import { requestId } from "@/middleware/requestId"; 11 | import { loggingMiddleware } from "@/middleware/loggingMiddleware"; 12 | import { compressionMiddleware } from "@/middleware/performanceMiddleware"; 13 | import { cache } from "@/middleware/cacheMiddleware"; 14 | import { metricsMiddleware } from "@/middleware/monitoringMiddleware"; 15 | import monitoringRoutes from "@/routes/monitoring.routes"; 16 | import { ErrorMonitoringService } from "@/services/errorMonitoring.service"; 17 | import { Request, Response, NextFunction, ErrorRequestHandler } from "express"; 18 | import swaggerUi from 'swagger-ui-express'; 19 | import { specs } from './docs/swagger'; 20 | import { notFoundHandler } from './middleware/notFound'; 21 | 22 | const app = express(); 23 | 24 | // Initialize error monitoring 25 | ErrorMonitoringService.getInstance(); 26 | 27 | // Group middleware by function 28 | const setupMiddleware = (app: express.Application) => { 29 | // Security 30 | app.use(requestId); 31 | setupSecurityHeaders(app as express.Express); 32 | app.use(cors({ origin: ENV.FRONTEND_URL, credentials: true })); 33 | 34 | // Performance 35 | app.use(compressionMiddleware); 36 | app.use(express.json({ limit: "10kb" })); 37 | 38 | // Monitoring 39 | app.use(loggingMiddleware); 40 | app.use(metricsMiddleware); 41 | 42 | // Rate Limiting 43 | app.use("/api/auth", authLimiter); 44 | app.use("/api", apiLimiter); 45 | }; 46 | 47 | setupMiddleware(app); 48 | 49 | // Routes 50 | app.get("/", (req, res) => { 51 | res.json({ message: "🚀 Hello from express-boilerplate Backend!" }); 52 | }); 53 | 54 | // Health Check 55 | app.get("/health", (req, res) => { 56 | res.json({ 57 | status: "ok", 58 | timestamp: new Date(), 59 | uptime: process.uptime(), 60 | memoryUsage: process.memoryUsage(), 61 | }); 62 | }); 63 | 64 | app.use("/api/auth", authRoutes); 65 | app.use("/api/users", userRoutes); 66 | 67 | // Move Swagger docs before error handler 68 | const swaggerOptions = { 69 | explorer: true, 70 | swaggerOptions: { 71 | persistAuthorization: true, 72 | displayRequestDuration: true, 73 | docExpansion: 'none', 74 | filter: true, 75 | showExtensions: true, 76 | showCommonExtensions: true, 77 | tryItOutEnabled: true 78 | }, 79 | customCss: '.swagger-ui .topbar { display: none }', 80 | customSiteTitle: "Express TypeScript API Documentation" 81 | }; 82 | 83 | // Move monitoring routes before error handler 84 | app.use("/api/monitoring", monitoringRoutes); 85 | 86 | // Add Swagger documentation route at root level 87 | app.use('/api-docs', swaggerUi.serve); 88 | app.get('/api-docs', swaggerUi.setup(specs, swaggerOptions)); 89 | 90 | // Error Handler should be last 91 | const errorMiddleware: ErrorRequestHandler = (err, req, res, next) => { 92 | return errorHandler(err, req, res, next); 93 | }; 94 | 95 | app.use(errorMiddleware); 96 | 97 | // Move cache middleware before error handler 98 | app.use("/api/users", cache({ duration: 300 })); 99 | 100 | // Monitoring routes 101 | app.use("/monitoring", monitoringRoutes); 102 | 103 | // Add this as the last middleware (before error handler) 104 | app.use(notFoundHandler); 105 | 106 | export default app; 107 | -------------------------------------------------------------------------------- /src/config/database.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | import { ENV } from "@/config/env"; 3 | 4 | const prisma = new PrismaClient({ 5 | // log only in development 6 | log: ENV.NODE_ENV === "development" ? ["query", "error", "warn"] : [], 7 | datasources: { 8 | db: { 9 | url: ENV.MYSQL_DATABASE_URL, 10 | }, 11 | }, 12 | }); 13 | 14 | // Soft shutdown handler 15 | const handleShutdown = async () => { 16 | console.log("Shutting down database connection"); 17 | await prisma.$disconnect(); 18 | }; 19 | 20 | process.on("SIGTERM", handleShutdown); 21 | process.on("SIGINT", handleShutdown); 22 | 23 | export default prisma; 24 | -------------------------------------------------------------------------------- /src/config/env.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import "dotenv/config"; 3 | 4 | const envSchema = z.object({ 5 | MYSQL_DATABASE_URL: z.string(), 6 | PORT: z 7 | .string() 8 | .transform(Number) 9 | .refine((n) => n >= 1024 && n <= 65535, { 10 | message: "Port must be between 1024 and 65535", 11 | }), 12 | NODE_ENV: z.enum(["development", "production", "test"]), 13 | JWT_SECRET: z.string().min(32), 14 | REFRESH_TOKEN_SECRET: z.string().min(32), 15 | JWT_EXPIRY: z.string().regex(/^\d+[smhd]$/), 16 | REFRESH_TOKEN_EXPIRY: z.string().regex(/^\d+[smhd]$/), 17 | FRONTEND_URL: z.string().url(), 18 | SMTP_HOST: process.env.NODE_ENV === "development" ? z.string().optional() : z.string(), 19 | SMTP_PORT: process.env.NODE_ENV === "development" ? z.string().transform(Number).optional() : z.string().transform(Number), 20 | SMTP_USER: process.env.NODE_ENV === "development" ? z.string().optional() : z.string(), 21 | SMTP_PASSWORD: process.env.NODE_ENV === "development" ? z.string().optional() : z.string(), 22 | SMTP_FROM: process.env.NODE_ENV === "development" ? z.string().email().optional() : z.string().email(), 23 | APP_NAME: process.env.NODE_ENV === "development" ? z.string().optional().default("Express Boilerplate") : z.string(), 24 | SERVER_URL: z.string().url(), 25 | PROMETHEUS_URL: z.string().url().optional().default('http://localhost:9090'), 26 | }); 27 | 28 | export const ENV = envSchema.parse(process.env); 29 | 30 | // Add validation for production environment 31 | if (process.env.NODE_ENV === 'production') { 32 | const requiredFields = [ 33 | 'SMTP_HOST', 34 | 'SMTP_PORT', 35 | 'SMTP_USER', 36 | 'SMTP_PASSWORD' 37 | ]; 38 | 39 | requiredFields.forEach(field => { 40 | if (!process.env[field]) { 41 | throw new Error(`Missing required env variable: ${field}`); 42 | } 43 | }); 44 | } 45 | -------------------------------------------------------------------------------- /src/config/logger.ts: -------------------------------------------------------------------------------- 1 | import winston from "winston"; 2 | import DailyRotateFile from "winston-daily-rotate-file"; 3 | import { ENV } from "./env"; 4 | 5 | const logLevel = ENV.NODE_ENV === "production" ? "info" : "debug"; 6 | 7 | const formatConfig = winston.format.combine( 8 | winston.format.timestamp(), 9 | winston.format.json(), 10 | winston.format.errors({ stack: true }), 11 | winston.format.metadata() 12 | ); 13 | 14 | const transports: winston.transport[] = [ 15 | new winston.transports.Console({ 16 | level: logLevel, 17 | format: winston.format.combine( 18 | winston.format.colorize(), 19 | winston.format.simple() 20 | ), 21 | }), 22 | ]; 23 | 24 | if (ENV.NODE_ENV === "production") { 25 | transports.push( 26 | new DailyRotateFile({ 27 | filename: "logs/error-%DATE%.log", 28 | datePattern: "YYYY-MM-DD", 29 | level: "error", 30 | maxSize: "20m", 31 | maxFiles: "14d", 32 | }), 33 | new DailyRotateFile({ 34 | filename: "logs/combined-%DATE%.log", 35 | datePattern: "YYYY-MM-DD", 36 | maxSize: "20m", 37 | maxFiles: "14d", 38 | }) 39 | ); 40 | } 41 | 42 | export const logger = winston.createLogger({ 43 | level: ENV.NODE_ENV === "production" ? "info" : "debug", 44 | format: formatConfig, 45 | transports, 46 | }); 47 | 48 | export const requestLogger = winston.createLogger({ 49 | format: formatConfig, 50 | transports: [ 51 | new (DailyRotateFile as any)({ 52 | filename: "logs/requests-%DATE%.log", 53 | datePattern: "YYYY-MM-DD", 54 | maxSize: "20m", 55 | maxFiles: "14d", 56 | }) as winston.transport, 57 | ], 58 | }); 59 | -------------------------------------------------------------------------------- /src/controllers/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from "express"; 2 | import { AuthService } from "@/services/auth.service"; 3 | import { BaseController } from "./base.controller"; 4 | import { AppError } from "@/utils/appError"; 5 | 6 | export class AuthController extends BaseController { 7 | constructor(private authService: AuthService) { 8 | super(); 9 | } 10 | 11 | signup = (req: Request, res: Response, next: NextFunction): void => { 12 | this.handleRequest(req, res, next, async () => { 13 | const { email, name, password } = req.body; 14 | return await this.authService.signup(email, name, password); 15 | }); 16 | }; 17 | 18 | login = (req: Request, res: Response, next: NextFunction): void => { 19 | this.handleRequest(req, res, next, async () => { 20 | const { email, password } = req.body; 21 | return await this.authService.login(email, password); 22 | }); 23 | }; 24 | 25 | logout = (req: Request, res: Response, next: NextFunction): void => { 26 | this.handleRequest(req, res, next, async () => { 27 | if (!req.user?.userId) { 28 | throw new AppError("Unauthorized", 401); 29 | } 30 | await this.authService.logout(req.user.userId); 31 | return { message: "Logged out successfully" }; 32 | }); 33 | }; 34 | 35 | refresh = (req: Request, res: Response, next: NextFunction): void => { 36 | this.handleRequest(req, res, next, async () => { 37 | const { refreshToken } = req.body; 38 | return await this.authService.refresh(refreshToken); 39 | }); 40 | }; 41 | 42 | verifyEmail = (req: Request, res: Response, next: NextFunction): void => { 43 | this.handleRequest(req, res, next, async () => { 44 | const { token } = req.params; 45 | return await this.authService.verifyEmail(token); 46 | }); 47 | }; 48 | 49 | resendVerification = (req: Request, res: Response, next: NextFunction): void => { 50 | this.handleRequest(req, res, next, async () => { 51 | const { email } = req.body; 52 | return await this.authService.resendVerificationEmail(email); 53 | }); 54 | }; 55 | 56 | forgotPassword = (req: Request, res: Response, next: NextFunction): void => { 57 | this.handleRequest(req, res, next, async () => { 58 | const { email } = req.body; 59 | return await this.authService.forgotPassword(email); 60 | }); 61 | }; 62 | 63 | resetPassword = (req: Request, res: Response, next: NextFunction): void => { 64 | this.handleRequest(req, res, next, async () => { 65 | const { token } = req.params; 66 | const { password } = req.body; 67 | return await this.authService.resetPassword(token, password); 68 | }); 69 | }; 70 | } 71 | -------------------------------------------------------------------------------- /src/controllers/base.controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from "express"; 2 | import { ApiResponse } from "@/utils/apiResponse"; 3 | 4 | export abstract class BaseController { 5 | protected async handleRequest( 6 | req: Request, 7 | res: Response, 8 | next: NextFunction, 9 | action: () => Promise 10 | ): Promise { 11 | try { 12 | const result = await action(); 13 | ApiResponse.success(res, result); 14 | } catch (error) { 15 | next(error); 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /src/controllers/monitoring.controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from "express"; 2 | import { MetricsService } from "@/services/metrics.service"; 3 | import { BaseController } from "./base.controller"; 4 | import { logger } from "@/config/logger"; 5 | import { AppError } from "@/utils/appError"; 6 | 7 | export class MonitoringController extends BaseController { 8 | constructor(private metricsService: MetricsService) { 9 | super(); 10 | } 11 | 12 | getMetrics = async (req: Request, res: Response, next: NextFunction): Promise => { 13 | try { 14 | logger.debug('Metrics endpoint called'); 15 | 16 | // Set headers for Prometheus scraping 17 | res.set('Content-Type', this.metricsService.getContentType()); 18 | res.set('Cache-Control', 'no-store, no-cache, must-revalidate, private'); 19 | 20 | const metrics = await this.metricsService.getMetrics(); 21 | logger.debug('Metrics generated', { metricsLength: metrics.length }); 22 | 23 | res.send(metrics); 24 | } catch (error) { 25 | logger.error('Error generating metrics', { error }); 26 | next(error); 27 | } 28 | }; 29 | 30 | getHealth = async (req: Request, res: Response, next: NextFunction): Promise => { 31 | await this.handleRequest(req, res, next, async () => { 32 | // Add more detailed health checks 33 | const health = { 34 | status: "ok", 35 | timestamp: new Date(), 36 | uptime: process.uptime(), 37 | memoryUsage: process.memoryUsage(), 38 | cpuUsage: process.cpuUsage(), 39 | nodeVersion: process.version, 40 | pid: process.pid 41 | }; 42 | 43 | return health; 44 | }); 45 | }; 46 | 47 | getReadiness = async (req: Request, res: Response, next: NextFunction): Promise => { 48 | await this.handleRequest(req, res, next, async () => { 49 | return { status: "ok" }; 50 | }); 51 | }; 52 | 53 | getLiveness = async (req: Request, res: Response, next: NextFunction): Promise => { 54 | await this.handleRequest(req, res, next, async () => { 55 | return { status: "ok" }; 56 | }); 57 | }; 58 | 59 | handleAlert = async (req: Request, res: Response, next: NextFunction): Promise => { 60 | await this.handleRequest(req, res, next, async () => { 61 | const alerts = req.body; 62 | 63 | return { 64 | status: "success", 65 | message: "Alert received and processed" 66 | }; 67 | }); 68 | }; 69 | 70 | simulateError = async (req: Request, res: Response, next: NextFunction): Promise => { 71 | await this.handleRequest(req, res, next, async () => { 72 | const random = Math.random(); 73 | 74 | if (random < 0.3) { 75 | throw new AppError("Simulated 500 Internal Server Error", 500); 76 | } else if (random < 0.6) { 77 | throw new AppError("Simulated 400 Bad Request", 400); 78 | } else { 79 | throw new AppError("Simulated 503 Service Unavailable", 503); 80 | } 81 | }); 82 | }; 83 | } 84 | -------------------------------------------------------------------------------- /src/controllers/user.controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from "express"; 2 | import { UserService } from "@/services/user.service"; 3 | import { BaseController } from "./base.controller"; 4 | import { AppError } from "@/utils/appError"; 5 | 6 | export class UserController extends BaseController { 7 | constructor(private userService: UserService) { 8 | super(); 9 | } 10 | 11 | getAll = async (req: Request, res: Response, next: NextFunction): Promise => { 12 | await this.handleRequest(req, res, next, async () => { 13 | return await this.userService.getAllUsers(); 14 | }); 15 | }; 16 | 17 | getUser = async (req: Request, res: Response, next: NextFunction): Promise => { 18 | await this.handleRequest(req, res, next, async () => { 19 | if (!req.user || (req.user.role !== "ADMIN" && req.user.userId !== req.params.id)) { 20 | throw new AppError("Not authorized to access this profile", 403); 21 | } 22 | return await this.userService.getUserById(req.params.id); 23 | }); 24 | }; 25 | 26 | create = async (req: Request, res: Response, next: NextFunction): Promise => { 27 | await this.handleRequest(req, res, next, async () => { 28 | return await this.userService.createUser(req.body); 29 | }); 30 | }; 31 | 32 | update = async (req: Request, res: Response, next: NextFunction): Promise => { 33 | await this.handleRequest(req, res, next, async () => { 34 | return await this.userService.updateUser(req.params.id, req.body); 35 | }); 36 | }; 37 | 38 | delete = async (req: Request, res: Response, next: NextFunction): Promise => { 39 | await this.handleRequest(req, res, next, async () => { 40 | await this.userService.deleteUser(req.params.id); 41 | return null; 42 | }); 43 | }; 44 | } 45 | -------------------------------------------------------------------------------- /src/decorators/singleton.ts: -------------------------------------------------------------------------------- 1 | export function singleton(constructor: T) { 2 | let instance: any = null; 3 | 4 | return class extends constructor { 5 | constructor(...args: any[]) { 6 | if (!instance) { 7 | super(...args); 8 | instance = this; 9 | } 10 | return instance; 11 | } 12 | }; 13 | } -------------------------------------------------------------------------------- /src/docs/swagger.ts: -------------------------------------------------------------------------------- 1 | import swaggerJsdoc from 'swagger-jsdoc'; 2 | import { version } from '../../package.json'; 3 | 4 | const options: swaggerJsdoc.Options = { 5 | definition: { 6 | openapi: '3.0.0', 7 | info: { 8 | title: 'Express TypeScript API', 9 | version, 10 | description: 'API documentation for Express TypeScript Boilerplate', 11 | license: { 12 | name: 'MIT', 13 | url: 'https://opensource.org/licenses/MIT', 14 | }, 15 | }, 16 | servers: [ 17 | { 18 | url: '/api', 19 | description: 'API server', 20 | }, 21 | ], 22 | components: { 23 | securitySchemes: { 24 | bearerAuth: { 25 | type: 'http', 26 | scheme: 'bearer', 27 | bearerFormat: 'JWT', 28 | }, 29 | }, 30 | responses: { 31 | UnauthorizedError: { 32 | description: 'Access token is missing or invalid', 33 | content: { 34 | 'application/json': { 35 | schema: { 36 | type: 'object', 37 | properties: { 38 | success: { type: 'boolean', example: false }, 39 | message: { type: 'string' }, 40 | code: { type: 'string' } 41 | } 42 | } 43 | } 44 | } 45 | } 46 | } 47 | }, 48 | security: [{ 49 | bearerAuth: [], 50 | }], 51 | }, 52 | apis: ['./src/routes/*.ts', './src/docs/schemas/*.yml'], 53 | }; 54 | 55 | export const specs = swaggerJsdoc(options); -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import app from "@/app"; 2 | import { ENV } from "@/config/env"; 3 | import { logger } from "@/config/logger"; 4 | import prisma from "@/config/database"; 5 | import { WebSocketService } from "@/services/websocket.service"; 6 | 7 | const server = app.listen(ENV.PORT, () => { 8 | logger.info(`Server running on port ${ENV.PORT} in ${ENV.NODE_ENV} mode`); 9 | }); 10 | 11 | // Initialize WebSocket service 12 | WebSocketService.getInstance(server); 13 | 14 | // Graceful shutdown handler 15 | const shutdown = async () => { 16 | logger.info("Shutdown signal received"); 17 | 18 | // Add WebSocket cleanup 19 | const wsService = WebSocketService.getInstance(); 20 | wsService.broadcast({ type: 'shutdown', data: { message: 'Server shutting down' } }); 21 | 22 | // Add connection draining 23 | app.disable('connection'); // Stop accepting new connections 24 | 25 | // Add timeout for existing connections 26 | const connectionDrainTimeout = setTimeout(() => { 27 | logger.warn('Connection drain timeout reached, forcing shutdown'); 28 | process.exit(1); 29 | }, 10000); 30 | 31 | server.close(async () => { 32 | logger.info("HTTP server closed"); 33 | 34 | try { 35 | await prisma.$disconnect(); 36 | logger.info("Database connections closed"); 37 | 38 | process.exit(0); 39 | } catch (err) { 40 | logger.error("Error during shutdown:", err); 41 | process.exit(1); 42 | } 43 | }); 44 | 45 | // Force shutdown after 30 seconds 46 | setTimeout(() => { 47 | logger.error( 48 | "Could not close connections in time, forcefully shutting down" 49 | ); 50 | process.exit(1); 51 | }, 30000); 52 | }; 53 | 54 | process.on("SIGTERM", shutdown); 55 | process.on("SIGINT", shutdown); 56 | 57 | export default server; 58 | -------------------------------------------------------------------------------- /src/middleware/authMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from "express"; 2 | import jwt from "jsonwebtoken"; 3 | import { AppError } from "@/utils/appError"; 4 | import { ErrorCode } from "@/utils/errorCodes"; 5 | import { logger } from "@/config/logger"; 6 | import { ENV } from "@/config/env"; 7 | 8 | interface JwtPayload { 9 | userId: string; 10 | role: string; 11 | } 12 | 13 | declare global { 14 | namespace Express { 15 | interface Request { 16 | user: JwtPayload; 17 | } 18 | } 19 | } 20 | 21 | export const requireAuth = ( 22 | req: Request, 23 | res: Response, 24 | next: NextFunction 25 | ): void => { 26 | try { 27 | const token = req.headers.authorization?.split(" ")[1]; 28 | if (!token) { 29 | throw new AppError("No token provided", 401, ErrorCode.UNAUTHORIZED); 30 | } 31 | try { 32 | const decoded = jwt.verify(token, ENV.JWT_SECRET) as JwtPayload; 33 | req.user = decoded; 34 | next(); 35 | } catch (error) { 36 | logger.warn({ 37 | message: "Invalid token", 38 | context: "AuthMiddleware.requireAuth", 39 | error: error instanceof Error ? error.message : "Unknown error", 40 | }); 41 | throw new AppError( 42 | "Unauthorized - Invalid token", 43 | 401, 44 | ErrorCode.INVALID_TOKEN 45 | ); 46 | } 47 | } catch (error) { 48 | next(error); 49 | } 50 | }; 51 | 52 | export const requireRole = (roles: string[]) => { 53 | return (req: Request, res: Response, next: NextFunction): void => { 54 | try { 55 | if (!req.user?.role || !roles.includes(req.user.role)) { 56 | logger.warn({ 57 | message: "Insufficient permissions", 58 | context: "AuthMiddleware.requireRole", 59 | requiredRoles: roles, 60 | userRole: req.user?.role, 61 | userId: req.user?.userId, 62 | }); 63 | throw new AppError( 64 | "Forbidden - Insufficient permissions", 65 | 403, 66 | ErrorCode.FORBIDDEN 67 | ); 68 | } 69 | next(); 70 | } catch (error) { 71 | next(error); 72 | } 73 | }; 74 | }; 75 | -------------------------------------------------------------------------------- /src/middleware/cacheMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from "express"; 2 | import { ENV } from "@/config/env"; 3 | 4 | interface CacheOptions { 5 | duration?: number; 6 | private?: boolean; 7 | } 8 | 9 | export const cache = (options: CacheOptions = {}) => { 10 | const duration = options.duration || 300; // 5 minutes default 11 | 12 | return (req: Request, res: Response, next: NextFunction) => { 13 | if (ENV.NODE_ENV === "production" && req.method === "GET") { 14 | res.set( 15 | "Cache-Control", 16 | `${options.private ? "private" : "public"}, max-age=${duration}` 17 | ); 18 | } else { 19 | res.set("Cache-Control", "no-store"); 20 | } 21 | next(); 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /src/middleware/errorHandler.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from "express"; 2 | import { AppError } from "@/utils/appError"; 3 | import { ApiResponse } from "@/utils/apiResponse"; 4 | import { logger } from "@/config/logger"; 5 | import { MetricsService } from "@/services/metrics.service"; 6 | 7 | const metricsService = new MetricsService(); 8 | 9 | export const errorHandler = ( 10 | error: Error, 11 | req: Request, 12 | res: Response, 13 | _next: NextFunction 14 | ): void => { 15 | logger.error({ 16 | message: error.message, 17 | stack: error.stack, 18 | context: "ErrorHandler", 19 | }); 20 | 21 | const statusCode = error instanceof AppError ? error.statusCode : 500; 22 | const route = req.route?.path || req.path || "/unknown"; 23 | 24 | metricsService.recordHttpRequest( 25 | req.method, 26 | route, 27 | statusCode, 28 | 0 29 | ); 30 | 31 | if (error instanceof AppError) { 32 | ApiResponse.error(res, error.message, error.statusCode); 33 | return; 34 | } 35 | 36 | ApiResponse.error(res, "Internal server error", 500); 37 | }; 38 | -------------------------------------------------------------------------------- /src/middleware/loggingMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from "express"; 2 | import { logger } from "@/config/logger"; 3 | 4 | export const loggingMiddleware = ( 5 | req: Request, 6 | res: Response, 7 | next: NextFunction 8 | ) => { 9 | const startTime = Date.now(); 10 | 11 | res.on("finish", () => { 12 | const logData = { 13 | requestId: req.requestId, 14 | method: req.method, 15 | url: req.originalUrl, 16 | status: res.statusCode, 17 | duration: `${Date.now() - startTime}ms`, 18 | userAgent: req.get("user-agent"), 19 | ip: req.ip, 20 | context: "HttpRequest", 21 | userId: req.user?.userId, 22 | query: Object.keys(req.query).length ? req.query : undefined, 23 | body: Object.keys(req.body || {}).length ? req.body : undefined, 24 | }; 25 | 26 | if (res.statusCode >= 400) { 27 | logger.error("Request failed", logData); 28 | } else { 29 | logger.info("Request completed", logData); 30 | } 31 | }); 32 | 33 | next(); 34 | }; 35 | -------------------------------------------------------------------------------- /src/middleware/monitoringMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import { MetricsService } from "@/services/metrics.service"; 3 | import responseTime from "response-time"; 4 | import { logger } from "@/config/logger"; 5 | 6 | const metricsService = new MetricsService(); 7 | 8 | export const metricsMiddleware = responseTime((req: Request, res: Response, time: number) => { 9 | try { 10 | let route = req.route?.path || req.path || "/unknown"; 11 | 12 | if (req.params) { 13 | Object.keys(req.params).forEach(param => { 14 | route = route.replace(req.params[param], `:${param}`); 15 | }); 16 | } 17 | 18 | if (route.startsWith('/monitoring/metrics')) { 19 | return; 20 | } 21 | 22 | metricsService.recordHttpRequest( 23 | req.method, 24 | route, 25 | res.statusCode, 26 | time / 1000 27 | ); 28 | 29 | if (res.statusCode >= 400) { 30 | metricsService.recordHttpError( 31 | req.method, 32 | route, 33 | res.statusCode 34 | ); 35 | } 36 | 37 | const now = Date.now(); 38 | if (!lastStatsUpdate || now - lastStatsUpdate >= 15000) { 39 | metricsService.updateNodeStats(); 40 | lastStatsUpdate = now; 41 | } 42 | 43 | } catch (error) { 44 | logger.error('Error recording metrics', { error }); 45 | } 46 | }); 47 | 48 | let lastStatsUpdate: number | null = null; 49 | -------------------------------------------------------------------------------- /src/middleware/notFound.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import { ApiResponse } from '../utils/apiResponse'; 3 | 4 | /** 5 | * Middleware to handle 404 Not Found errors 6 | * This should be mounted after all other routes 7 | */ 8 | export const notFoundHandler = (req: Request, res: Response) => { 9 | ApiResponse.error(res, '🔍 Ooops! Looks like you are lost. 🗺️', 404); 10 | }; -------------------------------------------------------------------------------- /src/middleware/performanceMiddleware.ts: -------------------------------------------------------------------------------- 1 | import compression from "compression"; 2 | import { Request } from "express"; 3 | 4 | // Skip compressing responses for small payloads 5 | const shouldCompress = (req: Request, res: any) => { 6 | if (req.headers["x-no-compression"]) { 7 | return false; 8 | } 9 | return compression.filter(req, res); 10 | }; 11 | 12 | export const compressionMiddleware = compression({ 13 | filter: shouldCompress, 14 | level: 6, // Default compression level 15 | threshold: 1024, // Only compress responses above 1KB 16 | }); 17 | -------------------------------------------------------------------------------- /src/middleware/rateLimiter.ts: -------------------------------------------------------------------------------- 1 | import { ENV } from "@/config/env"; 2 | import rateLimit from "express-rate-limit"; 3 | 4 | export const authLimiter = rateLimit({ 5 | windowMs: 15 * 60 * 1000, // 15 minutes 6 | max: 50, // Reduced from 100 to 50 attempts per window 7 | message: { 8 | success: false, 9 | message: "Too many login attempts, please try again later", 10 | }, 11 | standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers 12 | legacyHeaders: false, // Disable the `X-RateLimit-*` headers 13 | keyGenerator: (req) => { 14 | return req.ip || req.headers['x-forwarded-for'] as string; 15 | }, 16 | skipSuccessfulRequests: true // Only count failed attempts 17 | }); 18 | 19 | export const apiLimiter = rateLimit({ 20 | windowMs: 15 * 60 * 1000, 21 | max: 50, // Reduced from 100 to 50 requests per 15 minutes 22 | message: { 23 | success: false, 24 | message: "Too many requests, please try again later", 25 | }, 26 | standardHeaders: true, 27 | legacyHeaders: false, 28 | skip: (req) => { 29 | return Boolean( 30 | req.path.startsWith('/monitoring') || // TODO: Skip all monitoring endpoints for now 31 | req.headers['user-agent']?.includes('Prometheus') 32 | ); 33 | } 34 | }); 35 | 36 | export const verificationLimiter = rateLimit({ 37 | windowMs: 60 * 60 * 1000, // 1 hour 38 | max: 3, // 3 attempts per hour 39 | message: { 40 | success: false, 41 | message: "Too many verification attempts, please try again later", 42 | }, 43 | }); 44 | -------------------------------------------------------------------------------- /src/middleware/requestId.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from "express"; 2 | import { v4 as uuidv4 } from "uuid"; 3 | 4 | declare global { 5 | namespace Express { 6 | interface Request { 7 | requestId: string; 8 | } 9 | } 10 | } 11 | 12 | export const requestId = (req: Request, res: Response, next: NextFunction) => { 13 | req.requestId = (req.headers["x-request-id"] as string) || uuidv4(); 14 | res.setHeader("X-Request-ID", req.requestId); 15 | next(); 16 | }; 17 | -------------------------------------------------------------------------------- /src/middleware/securityHeaders.ts: -------------------------------------------------------------------------------- 1 | import helmet from "helmet"; 2 | import { Express } from "express"; 3 | import { ENV } from "@/config/env"; 4 | 5 | export const setupSecurityHeaders = (app: Express) => { 6 | // Remove the X-Powered-By header to avoid exposing the technology stack 7 | app.disable("x-powered-by"); 8 | 9 | // Configure Helmet middleware with comprehensive security headers 10 | app.use( 11 | helmet({ 12 | // Content Security Policy (CSP) - Controls which resources can be loaded 13 | contentSecurityPolicy: { 14 | directives: { 15 | defaultSrc: ["'self'"], // Only allow resources from same origin 16 | scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'"], // Required for Swagger UI 17 | styleSrc: ["'self'", "'unsafe-inline'"], // Allow inline styles for Swagger UI 18 | imgSrc: ["'self'", "data:", "https:"], // Allow images from same origin, data URIs, and HTTPS 19 | connectSrc: ["'self'", ENV.FRONTEND_URL], // Allow API calls to same origin and frontend 20 | fontSrc: ["'self'", "https:", "data:"], // Allow fonts from same origin, HTTPS, and data URIs 21 | objectSrc: ["'none'"], // Block , , and elements 22 | mediaSrc: ["'none'"], // Block media elements 23 | frameSrc: ["'none'"], // Block iframes 24 | frameAncestors: ["'none'"], // Prevent site from being embedded 25 | formAction: ["'self'"], // Only allow forms to submit to same origin 26 | ...(ENV.NODE_ENV === 'production' ? { 27 | upgradeInsecureRequests: [], // Force HTTPS in production 28 | blockAllMixedContent: [] // Block mixed content in production 29 | } : {}) 30 | } 31 | }, 32 | // Cross-Origin Policies 33 | crossOriginEmbedderPolicy: false, // Required for Swagger UI 34 | crossOriginOpenerPolicy: { policy: "same-origin" }, // Isolate cross-origin windows 35 | crossOriginResourcePolicy: { policy: "same-origin" }, // Restrict cross-origin resource sharing 36 | 37 | // Browser Feature Policies 38 | dnsPrefetchControl: { allow: false }, // Disable DNS prefetching 39 | frameguard: { action: "deny" }, // Prevent clickjacking 40 | hsts: { // HTTP Strict Transport Security 41 | maxAge: 31536000, // 1 year in seconds 42 | includeSubDomains: true, // Apply to subdomains 43 | preload: true // Allow preloading HSTS 44 | }, 45 | ieNoOpen: true, // Prevent IE from executing downloads 46 | noSniff: true, // Prevent MIME type sniffing 47 | originAgentCluster: true, // Improve performance isolation 48 | permittedCrossDomainPolicies: { permittedPolicies: "none" },// Restrict Adobe Flash and PDFs 49 | referrerPolicy: { policy: "no-referrer" }, // Control referrer information 50 | xssFilter: true // Enable XSS filtering 51 | }) 52 | ); 53 | 54 | // Additional custom security headers 55 | app.use((req, res, next) => { 56 | // Restrict browser features and APIs 57 | res.setHeader('Permissions-Policy', 58 | 'accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()'); 59 | res.setHeader('X-Content-Type-Options', 'nosniff'); // Prevent MIME type sniffing 60 | res.setHeader('X-Frame-Options', 'DENY'); // Prevent clickjacking 61 | res.setHeader('X-XSS-Protection', '1; mode=block'); // Enable XSS protection 62 | next(); 63 | }); 64 | 65 | // Production-specific CORS configuration 66 | if (ENV.NODE_ENV === 'production') { 67 | app.use((req, res, next) => { 68 | const allowedOrigins = [ENV.FRONTEND_URL]; // Whitelist of allowed origins 69 | const origin = req.headers.origin; 70 | 71 | // Only allow requests from whitelisted origins 72 | if (origin && allowedOrigins.includes(origin)) { 73 | res.setHeader('Access-Control-Allow-Origin', origin); 74 | } 75 | next(); 76 | }); 77 | } 78 | }; 79 | -------------------------------------------------------------------------------- /src/middleware/validateRequest.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from "express"; 2 | import { AnyZodObject, ZodError } from "zod"; 3 | import { ValidationError } from "@/utils/errorHandler"; 4 | 5 | export const validateRequest = (schema: AnyZodObject) => { 6 | return (req: Request, res: Response, next: NextFunction): void => { 7 | try { 8 | schema.parse({ 9 | body: req.body, 10 | query: req.query, 11 | params: req.params, 12 | headers: req.headers 13 | }); 14 | next(); 15 | } catch (error) { 16 | if (error instanceof ZodError) { 17 | next(new ValidationError(error.errors[0]?.message || "Validation failed")); 18 | return; 19 | } 20 | next(new ValidationError("Invalid request data")); 21 | } 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /src/routes/auth.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import { AuthController } from "@/controllers/auth.controller"; 3 | import { AuthService } from "@/services/auth.service"; 4 | import { validateRequest } from "@/middleware/validateRequest"; 5 | import { loginSchema, signupSchema, verifyEmailSchema, resendVerificationSchema, forgotPasswordSchema, resetPasswordSchema } from "@/validators/auth.validator"; 6 | import { requireAuth } from "@/middleware/authMiddleware"; 7 | import { verificationLimiter } from "@/middleware/rateLimiter"; 8 | 9 | const router = Router(); 10 | 11 | // Initialize services and controller 12 | const authService = new AuthService(); 13 | const authController = new AuthController(authService); 14 | 15 | // Routes 16 | /** 17 | * @swagger 18 | * tags: 19 | * name: Auth 20 | * description: Authentication endpoints 21 | */ 22 | 23 | /** 24 | * @swagger 25 | * /auth/signup: 26 | * post: 27 | * summary: Register a new user 28 | * tags: [Auth] 29 | * requestBody: 30 | * required: true 31 | * content: 32 | * application/json: 33 | * schema: 34 | * type: object 35 | * required: 36 | * - email 37 | * - name 38 | * - password 39 | * properties: 40 | * email: 41 | * type: string 42 | * format: email 43 | * name: 44 | * type: string 45 | * minLength: 2 46 | * password: 47 | * type: string 48 | * format: password 49 | * minLength: 8 50 | * responses: 51 | * 201: 52 | * description: User created successfully 53 | * 400: 54 | * description: Invalid input 55 | * 409: 56 | * description: Email already exists 57 | */ 58 | router.post("/signup", validateRequest(signupSchema), authController.signup); 59 | 60 | /** 61 | * @swagger 62 | * /auth/login: 63 | * post: 64 | * summary: Login user 65 | * tags: [Auth] 66 | * requestBody: 67 | * required: true 68 | * content: 69 | * application/json: 70 | * schema: 71 | * type: object 72 | * required: 73 | * - email 74 | * - password 75 | * properties: 76 | * email: 77 | * type: string 78 | * format: email 79 | * password: 80 | * type: string 81 | * format: password 82 | * responses: 83 | * 200: 84 | * description: Login successful 85 | * content: 86 | * application/json: 87 | * schema: 88 | * type: object 89 | * properties: 90 | * accessToken: 91 | * type: string 92 | * refreshToken: 93 | * type: string 94 | * 401: 95 | * description: Invalid credentials 96 | */ 97 | router.post("/login", validateRequest(loginSchema), authController.login); 98 | 99 | /** 100 | * @swagger 101 | * /auth/refresh: 102 | * post: 103 | * summary: Refresh access token 104 | * tags: [Auth] 105 | * requestBody: 106 | * required: true 107 | * content: 108 | * application/json: 109 | * schema: 110 | * type: object 111 | * required: 112 | * - refreshToken 113 | * properties: 114 | * refreshToken: 115 | * type: string 116 | * responses: 117 | * 200: 118 | * description: New access token 119 | * 401: 120 | * description: Invalid refresh token 121 | */ 122 | router.post("/refresh", authController.refresh); 123 | 124 | /** 125 | * @swagger 126 | * /auth/logout: 127 | * post: 128 | * summary: Logout user 129 | * tags: [Auth] 130 | * security: 131 | * - bearerAuth: [] 132 | * responses: 133 | * 200: 134 | * description: Successfully logged out 135 | * 401: 136 | * description: Unauthorized - Invalid or missing token 137 | */ 138 | router.post("/logout", requireAuth, authController.logout); 139 | 140 | /** 141 | * @swagger 142 | * /auth/verify-email/{token}: 143 | * get: 144 | * summary: Verify email address 145 | * tags: [Auth] 146 | * parameters: 147 | * - in: path 148 | * name: token 149 | * required: true 150 | * schema: 151 | * type: string 152 | * description: Email verification token 153 | * responses: 154 | * 200: 155 | * description: Email verified successfully 156 | * 400: 157 | * description: Invalid token 158 | * 404: 159 | * description: Token not found 160 | */ 161 | router.get("/verify-email/:token", validateRequest(verifyEmailSchema), authController.verifyEmail); 162 | 163 | /** 164 | * @swagger 165 | * /auth/send-email-verification: 166 | * post: 167 | * summary: Resend verification email 168 | * tags: [Auth] 169 | * requestBody: 170 | * required: true 171 | * content: 172 | * application/json: 173 | * schema: 174 | * type: object 175 | * required: 176 | * - email 177 | * properties: 178 | * email: 179 | * type: string 180 | * format: email 181 | * responses: 182 | * 200: 183 | * description: Verification email sent 184 | * 429: 185 | * description: Too many requests 186 | */ 187 | router.post( 188 | "/send-email-verification", 189 | verificationLimiter, 190 | validateRequest(resendVerificationSchema), 191 | authController.resendVerification 192 | ); 193 | 194 | /** 195 | * @swagger 196 | * /auth/forgot-password: 197 | * post: 198 | * summary: Request password reset 199 | * tags: [Auth] 200 | * requestBody: 201 | * required: true 202 | * content: 203 | * application/json: 204 | * schema: 205 | * type: object 206 | * required: 207 | * - email 208 | * properties: 209 | * email: 210 | * type: string 211 | * format: email 212 | * responses: 213 | * 200: 214 | * description: Reset email sent if email exists 215 | */ 216 | router.post("/forgot-password", validateRequest(forgotPasswordSchema), authController.forgotPassword); 217 | 218 | /** 219 | * @swagger 220 | * /auth/reset-password/{token}: 221 | * post: 222 | * summary: Reset password 223 | * tags: [Auth] 224 | * parameters: 225 | * - in: path 226 | * name: token 227 | * required: true 228 | * schema: 229 | * type: string 230 | * description: Password reset token 231 | * requestBody: 232 | * required: true 233 | * content: 234 | * application/json: 235 | * schema: 236 | * type: object 237 | * required: 238 | * - password 239 | * properties: 240 | * password: 241 | * type: string 242 | * format: password 243 | * minLength: 8 244 | * responses: 245 | * 200: 246 | * description: Password reset successful 247 | * 400: 248 | * description: Invalid token or password 249 | * 404: 250 | * description: Token not found 251 | */ 252 | router.post("/reset-password/:token", validateRequest(resetPasswordSchema), authController.resetPassword); 253 | 254 | export default router; 255 | -------------------------------------------------------------------------------- /src/routes/monitoring.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import { MonitoringController } from "@/controllers/monitoring.controller"; 3 | import { MetricsService } from "@/services/metrics.service"; 4 | import { requireAuth } from "@/middleware/authMiddleware"; 5 | 6 | const router = Router(); 7 | const metricsService = new MetricsService(); 8 | const monitoringController = new MonitoringController(metricsService); 9 | 10 | /** 11 | * @swagger 12 | * tags: 13 | * name: Monitoring 14 | * description: System monitoring and health check endpoints 15 | */ 16 | 17 | /** 18 | * @swagger 19 | * /monitoring/metrics: 20 | * get: 21 | * summary: Get system metrics 22 | * tags: [Monitoring] 23 | * security: 24 | * - bearerAuth: [] 25 | * responses: 26 | * 200: 27 | * description: Prometheus metrics in text format 28 | * content: 29 | * text/plain: 30 | * schema: 31 | * type: string 32 | * 401: 33 | * description: Unauthorized 34 | */ 35 | router.get("/metrics", monitoringController.getMetrics); // TODO: Add Authentication 36 | 37 | /** 38 | * @swagger 39 | * /monitoring/health: 40 | * get: 41 | * summary: Check system health 42 | * tags: [Monitoring] 43 | * responses: 44 | * 200: 45 | * description: System health information 46 | * content: 47 | * application/json: 48 | * schema: 49 | * type: object 50 | * properties: 51 | * status: 52 | * type: string 53 | * example: ok 54 | * timestamp: 55 | * type: string 56 | * format: date-time 57 | * uptime: 58 | * type: number 59 | * memoryUsage: 60 | * type: object 61 | */ 62 | router.get("/health", monitoringController.getHealth); 63 | 64 | /** 65 | * @swagger 66 | * /monitoring/readiness: 67 | * get: 68 | * summary: Check if application is ready to handle traffic 69 | * tags: [Monitoring] 70 | * responses: 71 | * 200: 72 | * description: Application is ready 73 | * content: 74 | * application/json: 75 | * schema: 76 | * type: object 77 | * properties: 78 | * status: 79 | * type: string 80 | * example: ok 81 | */ 82 | router.get("/readiness", monitoringController.getReadiness); 83 | 84 | /** 85 | * @swagger 86 | * /monitoring/liveness: 87 | * get: 88 | * summary: Check if application is alive 89 | * tags: [Monitoring] 90 | * responses: 91 | * 200: 92 | * description: Application is alive 93 | * content: 94 | * application/json: 95 | * schema: 96 | * type: object 97 | * properties: 98 | * status: 99 | * type: string 100 | * example: ok 101 | */ 102 | router.get("/liveness", monitoringController.getLiveness); 103 | 104 | /** 105 | * @swagger 106 | * /monitoring/alerts: 107 | * post: 108 | * summary: Receive alerts from AlertManager 109 | * tags: [Monitoring] 110 | * security: 111 | * - bearerAuth: [] 112 | * requestBody: 113 | * required: true 114 | * content: 115 | * application/json: 116 | * schema: 117 | * type: object 118 | * properties: 119 | * alerts: 120 | * type: array 121 | * items: 122 | * type: object 123 | * responses: 124 | * 200: 125 | * description: Alert received and processed 126 | * 401: 127 | * description: Unauthorized 128 | */ 129 | router.post("/alerts", monitoringController.handleAlert); 130 | 131 | /** 132 | * @swagger 133 | * /monitoring/simulate-error: 134 | * get: 135 | * summary: Simulate random errors (for testing) 136 | * tags: [Monitoring] 137 | * responses: 138 | * 400: 139 | * description: Bad Request Error 140 | * 500: 141 | * description: Internal Server Error 142 | * 503: 143 | * description: Service Unavailable 144 | */ 145 | router.get("/simulate-error", monitoringController.simulateError); 146 | 147 | router.get("/trigger-gc", async (req, res) => { 148 | if (global.gc) { 149 | global.gc(); 150 | res.json({ message: "GC triggered" }); 151 | } else { 152 | res.status(400).json({ message: "GC not exposed. Run Node with --expose-gc flag" }); 153 | } 154 | }); 155 | 156 | router.get("/simulate-memory-leak", (req, res) => { 157 | const arr: any[] = []; 158 | for (let i = 0; i < 1000000; i++) { 159 | arr.push(new Array(1000).fill('test')); 160 | } 161 | res.json({ message: "Memory leak simulated" }); 162 | }); 163 | 164 | export default router; 165 | -------------------------------------------------------------------------------- /src/routes/user.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import { UserController } from "@/controllers/user.controller"; 3 | import { UserService } from "@/services/user.service"; 4 | import { validateRequest } from "@/middleware/validateRequest"; 5 | import { requireAuth, requireRole } from "@/middleware/authMiddleware"; 6 | import { 7 | createUserSchema, 8 | updateUserSchema, 9 | } from "@/validators/user.validator"; 10 | import { cache } from "@/middleware/cacheMiddleware"; 11 | 12 | const router = Router(); 13 | const userService = new UserService(); 14 | const userController = new UserController(userService); 15 | 16 | /** 17 | * @swagger 18 | * tags: 19 | * name: Users 20 | * description: User management endpoints 21 | */ 22 | 23 | /** 24 | * @swagger 25 | * components: 26 | * schemas: 27 | * User: 28 | * type: object 29 | * properties: 30 | * id: 31 | * type: string 32 | * format: uuid 33 | * name: 34 | * type: string 35 | * email: 36 | * type: string 37 | * format: email 38 | * role: 39 | * type: string 40 | * enum: [ADMIN, USER] 41 | * createdAt: 42 | * type: string 43 | * format: date-time 44 | * updatedAt: 45 | * type: string 46 | * format: date-time 47 | */ 48 | 49 | // Protected routes - all routes require authentication 50 | router.use(requireAuth); 51 | 52 | /** 53 | * @swagger 54 | * /users: 55 | * get: 56 | * summary: Get all users (Admin only) 57 | * tags: [Users] 58 | * security: 59 | * - bearerAuth: [] 60 | * responses: 61 | * 200: 62 | * description: List of users 63 | * content: 64 | * application/json: 65 | * schema: 66 | * type: array 67 | * items: 68 | * $ref: '#/components/schemas/User' 69 | * 401: 70 | * description: Unauthorized 71 | * 403: 72 | * description: Forbidden - Admin only 73 | */ 74 | router.get( 75 | "/", 76 | requireRole(["ADMIN"]), 77 | cache({ duration: 300 }), // Cache for 5 minutes 78 | userController.getAll 79 | ); 80 | 81 | /** 82 | * @swagger 83 | * /users/{id}: 84 | * get: 85 | * summary: Get user by ID 86 | * tags: [Users] 87 | * security: 88 | * - bearerAuth: [] 89 | * parameters: 90 | * - in: path 91 | * name: id 92 | * required: true 93 | * schema: 94 | * type: string 95 | * format: uuid 96 | * responses: 97 | * 200: 98 | * description: User details 99 | * content: 100 | * application/json: 101 | * schema: 102 | * $ref: '#/components/schemas/User' 103 | * 404: 104 | * description: User not found 105 | */ 106 | router.get( 107 | "/:id", 108 | cache({ duration: 60 }), // Cache for 1 minute 109 | userController.getUser 110 | ); 111 | 112 | /** 113 | * @swagger 114 | * /users: 115 | * post: 116 | * summary: Create new user (Admin only) 117 | * tags: [Users] 118 | * security: 119 | * - bearerAuth: [] 120 | * requestBody: 121 | * required: true 122 | * content: 123 | * application/json: 124 | * schema: 125 | * type: object 126 | * required: 127 | * - name 128 | * - email 129 | * - password 130 | * properties: 131 | * name: 132 | * type: string 133 | * minLength: 2 134 | * email: 135 | * type: string 136 | * format: email 137 | * password: 138 | * type: string 139 | * format: password 140 | * minLength: 8 141 | * role: 142 | * type: string 143 | * enum: [ADMIN, USER] 144 | * responses: 145 | * 201: 146 | * description: User created 147 | * 400: 148 | * description: Invalid input 149 | * 403: 150 | * description: Forbidden - Admin only 151 | */ 152 | router.post( 153 | "/", 154 | requireRole(["ADMIN"]), 155 | validateRequest(createUserSchema), 156 | userController.create 157 | ); 158 | 159 | /** 160 | * @swagger 161 | * /users/{id}: 162 | * patch: 163 | * summary: Update user (Admin only) 164 | * tags: [Users] 165 | * security: 166 | * - bearerAuth: [] 167 | * parameters: 168 | * - in: path 169 | * name: id 170 | * required: true 171 | * schema: 172 | * type: string 173 | * format: uuid 174 | * requestBody: 175 | * required: true 176 | * content: 177 | * application/json: 178 | * schema: 179 | * type: object 180 | * properties: 181 | * name: 182 | * type: string 183 | * minLength: 2 184 | * email: 185 | * type: string 186 | * format: email 187 | * role: 188 | * type: string 189 | * enum: [ADMIN, USER] 190 | * responses: 191 | * 200: 192 | * description: User updated 193 | * 400: 194 | * description: Invalid input 195 | * 403: 196 | * description: Forbidden - Admin only 197 | * 404: 198 | * description: User not found 199 | */ 200 | router.patch( 201 | "/:id", 202 | requireRole(["ADMIN"]), 203 | validateRequest(updateUserSchema), 204 | userController.update 205 | ); 206 | 207 | /** 208 | * @swagger 209 | * /users/{id}: 210 | * delete: 211 | * summary: Delete user (Admin only) 212 | * tags: [Users] 213 | * security: 214 | * - bearerAuth: [] 215 | * parameters: 216 | * - in: path 217 | * name: id 218 | * required: true 219 | * schema: 220 | * type: string 221 | * format: uuid 222 | * responses: 223 | * 200: 224 | * description: User deleted 225 | * 403: 226 | * description: Forbidden - Admin only 227 | * 404: 228 | * description: User not found 229 | */ 230 | router.delete("/:id", requireRole(["ADMIN"]), userController.delete); 231 | 232 | export default router; 233 | -------------------------------------------------------------------------------- /src/services/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | import bcrypt from "bcrypt"; 3 | import jwt from "jsonwebtoken"; 4 | import { ENV } from "@/config/env"; 5 | import { AppError } from "@/utils/appError"; 6 | import { logger } from "@/config/logger"; 7 | import { ErrorCode } from "@/utils/errorCodes"; 8 | import crypto from "crypto"; 9 | import { EmailService } from "./email.service"; 10 | 11 | const prisma = new PrismaClient(); 12 | 13 | export class AuthService { 14 | private emailService: EmailService; 15 | 16 | constructor() { 17 | this.emailService = new EmailService(); 18 | } 19 | 20 | private generateVerificationToken(): string { 21 | return crypto.randomBytes(32).toString("hex"); 22 | } 23 | 24 | async signup(email: string, name: string, password: string) { 25 | const existingUser = await prisma.user.findUnique({ where: { email } }); 26 | if (existingUser) { 27 | throw new AppError("Email already exists", 400, ErrorCode.ALREADY_EXISTS); 28 | } 29 | 30 | const hashedPassword = await bcrypt.hash(password, 10); 31 | const verificationToken = this.generateVerificationToken(); 32 | const verificationExpires = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24 hours 33 | 34 | const user = await prisma.user.create({ 35 | data: { 36 | email, 37 | name, 38 | password: hashedPassword, 39 | emailVerificationToken: verificationToken, 40 | emailVerificationExpires: verificationExpires, 41 | }, 42 | select: { 43 | id: true, 44 | email: true, 45 | name: true, 46 | role: true, 47 | createdAt: true, 48 | }, 49 | }); 50 | 51 | // Send verification email 52 | await this.emailService.sendVerificationEmail(email, name, verificationToken); 53 | 54 | return user; 55 | } 56 | 57 | async verifyEmail(token: string) { 58 | const user = await prisma.user.findFirst({ 59 | where: { 60 | emailVerificationToken: token, 61 | emailVerificationExpires: { 62 | gt: new Date(), 63 | }, 64 | emailVerified: null, 65 | }, 66 | }); 67 | 68 | if (!user) { 69 | throw new AppError( 70 | "Invalid or expired verification token", 71 | 400, 72 | ErrorCode.INVALID_TOKEN 73 | ); 74 | } 75 | 76 | await prisma.user.update({ 77 | where: { id: user.id }, 78 | data: { 79 | emailVerified: new Date(), 80 | emailVerificationToken: null, 81 | emailVerificationExpires: null, 82 | }, 83 | }); 84 | 85 | return { message: "Email verified successfully" }; 86 | } 87 | 88 | private async cleanupExpiredTokens() { 89 | await prisma.user.updateMany({ 90 | where: { 91 | emailVerificationExpires: { 92 | lt: new Date(), 93 | }, 94 | emailVerified: null, 95 | }, 96 | data: { 97 | emailVerificationToken: null, 98 | emailVerificationExpires: null, 99 | }, 100 | }); 101 | } 102 | 103 | async resendVerificationEmail(email: string) { 104 | // Clean up expired tokens first 105 | await this.cleanupExpiredTokens(); 106 | 107 | const user = await prisma.user.findUnique({ 108 | where: { email } 109 | }); 110 | 111 | if (!user) { 112 | throw new AppError("User not found", 404, ErrorCode.NOT_FOUND); 113 | } 114 | 115 | if (user.emailVerified) { 116 | throw new AppError( 117 | "Email is already verified", 118 | 400, 119 | ErrorCode.INVALID_REQUEST 120 | ); 121 | } 122 | 123 | const verificationToken = this.generateVerificationToken(); 124 | await prisma.user.update({ 125 | where: { id: user.id }, 126 | data: { 127 | emailVerificationToken: verificationToken, 128 | emailVerificationExpires: new Date(Date.now() + 24 * 60 * 60 * 1000), 129 | }, 130 | }); 131 | 132 | await this.emailService.sendVerificationEmail( 133 | user.email, 134 | user.name, 135 | verificationToken 136 | ); 137 | 138 | return { message: "Verification email sent" }; 139 | } 140 | 141 | async login(email: string, password: string) { 142 | const user = await prisma.user.findUnique({ where: { email } }); 143 | if (!user || !user.password) { 144 | throw new AppError( 145 | "Invalid credentials", 146 | 401, 147 | ErrorCode.INVALID_CREDENTIALS 148 | ); 149 | } 150 | 151 | if (!user.emailVerified) { 152 | throw new AppError( 153 | "Please verify your email before logging in", 154 | 401, 155 | ErrorCode.UNAUTHORIZED 156 | ); 157 | } 158 | 159 | const isPasswordValid = await bcrypt.compare(password, user.password); 160 | if (!isPasswordValid) { 161 | throw new AppError( 162 | "Invalid credentials", 163 | 401, 164 | ErrorCode.INVALID_CREDENTIALS 165 | ); 166 | } 167 | 168 | const accessToken = this.generateAccessToken(user.id, user.role); 169 | const refreshToken = this.generateRefreshToken(user.id); 170 | 171 | // Store refresh token in database 172 | await prisma.user.update({ 173 | where: { id: user.id }, 174 | data: { refreshToken }, 175 | }); 176 | 177 | return { 178 | user: { 179 | id: user.id, 180 | email: user.email, 181 | name: user.name, 182 | role: user.role, 183 | }, 184 | accessToken, 185 | refreshToken, 186 | }; 187 | } 188 | 189 | async refresh(refreshToken: string) { 190 | if (!refreshToken) { 191 | throw new AppError( 192 | "Refresh token is required", 193 | 400, 194 | ErrorCode.INVALID_TOKEN 195 | ); 196 | } 197 | 198 | try { 199 | const decoded = jwt.verify(refreshToken, ENV.REFRESH_TOKEN_SECRET) as { 200 | userId: string; 201 | }; 202 | 203 | logger.debug("Processing refresh token request", { 204 | userId: decoded.userId, 205 | context: "AuthService.refresh", 206 | }); 207 | 208 | const user = await prisma.user.findFirst({ 209 | where: { 210 | id: decoded.userId, 211 | refreshToken: refreshToken, 212 | }, 213 | }); 214 | 215 | if (!user) { 216 | throw new AppError( 217 | "Invalid refresh token", 218 | 401, 219 | ErrorCode.INVALID_TOKEN 220 | ); 221 | } 222 | 223 | const accessToken = this.generateAccessToken(user.id, user.role); 224 | const newRefreshToken = this.generateRefreshToken(user.id); 225 | 226 | await prisma.user.update({ 227 | where: { id: user.id }, 228 | data: { refreshToken: newRefreshToken }, 229 | }); 230 | 231 | return { 232 | accessToken, 233 | refreshToken: newRefreshToken, 234 | user: { 235 | id: user.id, 236 | email: user.email, 237 | name: user.name, 238 | role: user.role, 239 | }, 240 | }; 241 | } catch (error) { 242 | logger.error("Refresh token error", { 243 | error, 244 | context: "AuthService.refresh", 245 | }); 246 | throw new AppError("Invalid refresh token", 401, ErrorCode.INVALID_TOKEN); 247 | } 248 | } 249 | 250 | async logout(userId: string) { 251 | if (!userId) { 252 | throw new AppError("User ID is required", 400, ErrorCode.INVALID_INPUT); 253 | } 254 | 255 | try { 256 | await prisma.user.update({ 257 | where: { id: userId }, 258 | data: { refreshToken: null }, 259 | }); 260 | } catch (error) { 261 | logger.error("Logout error", { 262 | error, 263 | userId, 264 | context: "AuthService.logout", 265 | }); 266 | throw new AppError( 267 | "Failed to logout", 268 | 500, 269 | ErrorCode.INTERNAL_SERVER_ERROR 270 | ); 271 | } 272 | } 273 | 274 | private generateAccessToken(userId: string, role: string): string { 275 | return jwt.sign({ userId, role }, ENV.JWT_SECRET, { 276 | expiresIn: ENV.JWT_EXPIRY, 277 | }); 278 | } 279 | 280 | private generateRefreshToken(userId: string): string { 281 | return jwt.sign({ userId }, ENV.REFRESH_TOKEN_SECRET, { 282 | expiresIn: ENV.REFRESH_TOKEN_EXPIRY, 283 | }); 284 | } 285 | 286 | async forgotPassword(email: string) { 287 | const user = await prisma.user.findUnique({ where: { email } }); 288 | if (!user) { 289 | throw new AppError("User not found", 404, ErrorCode.NOT_FOUND); 290 | } 291 | 292 | const resetToken = this.generateVerificationToken(); 293 | const resetExpires = new Date(Date.now() + 60 * 60 * 1000); // 1 hour 294 | 295 | await prisma.user.update({ 296 | where: { id: user.id }, 297 | data: { 298 | passwordResetToken: resetToken, 299 | passwordResetExpires: resetExpires, 300 | }, 301 | }); 302 | 303 | try { 304 | await this.emailService.sendPasswordResetEmail( 305 | user.email, 306 | user.name, 307 | resetToken 308 | ); 309 | return { message: "Password reset email sent" }; 310 | } catch (error) { 311 | // If email fails, clear the reset token 312 | await prisma.user.update({ 313 | where: { id: user.id }, 314 | data: { 315 | passwordResetToken: null, 316 | passwordResetExpires: null, 317 | }, 318 | }); 319 | throw error; 320 | } 321 | } 322 | 323 | async resetPassword(token: string, newPassword: string) { 324 | const user = await prisma.user.findFirst({ 325 | where: { 326 | passwordResetToken: token, 327 | passwordResetExpires: { 328 | gt: new Date(), 329 | }, 330 | }, 331 | }); 332 | 333 | if (!user) { 334 | throw new AppError( 335 | "Invalid or expired reset token", 336 | 400, 337 | ErrorCode.INVALID_TOKEN 338 | ); 339 | } 340 | 341 | const hashedPassword = await bcrypt.hash(newPassword, 10); 342 | 343 | await prisma.user.update({ 344 | where: { id: user.id }, 345 | data: { 346 | password: hashedPassword, 347 | passwordResetToken: null, 348 | passwordResetExpires: null, 349 | }, 350 | }); 351 | 352 | return { message: "Password reset successfully" }; 353 | } 354 | } 355 | -------------------------------------------------------------------------------- /src/services/email.service.ts: -------------------------------------------------------------------------------- 1 | import nodemailer from "nodemailer"; 2 | import { ENV } from "@/config/env"; 3 | import { logger } from "@/config/logger"; 4 | import { getVerificationEmailTemplate, getPasswordResetEmailTemplate } from '@/templates/emails'; 5 | 6 | export class EmailService { 7 | private transporter!: nodemailer.Transporter; 8 | private readonly fromAddress: string; 9 | 10 | constructor() { 11 | // Production SMTP setup 12 | this.transporter = nodemailer.createTransport({ 13 | host: ENV.SMTP_HOST, 14 | port: ENV.SMTP_PORT, 15 | secure: ENV.SMTP_PORT === 465, 16 | auth: { 17 | user: ENV.SMTP_USER, 18 | pass: ENV.SMTP_PASSWORD, 19 | }, 20 | tls: { 21 | rejectUnauthorized: true 22 | } 23 | }); 24 | this.fromAddress = ENV.SMTP_FROM || 'noreply@example.com'; 25 | 26 | logger.info("Using SMTP configuration", { 27 | context: "EmailService.constructor", 28 | host: ENV.SMTP_HOST, 29 | }); 30 | 31 | // Add email template precompilation 32 | this.precompileTemplates(); 33 | 34 | // Add connection testing 35 | this.testConnection(); 36 | } 37 | 38 | private async testConnection() { 39 | try { 40 | await this.transporter.verify(); 41 | logger.info("SMTP connection verified"); 42 | } catch (error) { 43 | logger.error("SMTP connection failed", { error }); 44 | } 45 | } 46 | 47 | private precompileTemplates() { 48 | try { 49 | getVerificationEmailTemplate('test', 'test'); // Pre-compile by running once 50 | getPasswordResetEmailTemplate('test', 'test'); // Pre-compile by running once 51 | logger.info("Email templates precompiled successfully"); 52 | } catch (error) { 53 | logger.error("Failed to precompile email templates", { error }); 54 | } 55 | } 56 | 57 | async sendVerificationEmail( 58 | to: string, 59 | name: string, 60 | verificationToken: string 61 | ): Promise { 62 | const verificationUrl = `${ENV.SERVER_URL}/api/auth/verify-email/${verificationToken}`; // TODO: Change this to frontend URL 63 | 64 | try { 65 | const info = await this.transporter.sendMail({ 66 | from: this.fromAddress, 67 | to, 68 | subject: "Verify your email address", 69 | html: getVerificationEmailTemplate(name, verificationUrl), 70 | }); 71 | 72 | logger.info("Verification email sent", { 73 | context: "EmailService.sendVerificationEmail", 74 | to, 75 | messageId: info.messageId, 76 | }); 77 | } catch (error) { 78 | logger.error("Failed to send verification email", { 79 | context: "EmailService.sendVerificationEmail", 80 | error: error instanceof Error ? error.message : "Unknown error", 81 | to, 82 | }); 83 | throw error; 84 | } 85 | } 86 | 87 | async sendPasswordResetEmail( 88 | to: string, 89 | name: string, 90 | resetToken: string 91 | ): Promise { 92 | const resetUrl = `${ENV.SERVER_URL}/reset-password/${resetToken}`; // TODO: Change this to frontend URL 93 | 94 | try { 95 | const info = await this.transporter.sendMail({ 96 | from: this.fromAddress, 97 | to, 98 | subject: "Reset Your Password", 99 | html: getPasswordResetEmailTemplate(name, resetUrl), 100 | }); 101 | 102 | logger.info("Password reset email sent", { 103 | context: "EmailService.sendPasswordResetEmail", 104 | to, 105 | messageId: info.messageId, 106 | }); 107 | } catch (error) { 108 | logger.error("Failed to send password reset email", { 109 | context: "EmailService.sendPasswordResetEmail", 110 | error: error instanceof Error ? error.message : "Unknown error", 111 | to, 112 | }); 113 | throw error; 114 | } 115 | } 116 | } -------------------------------------------------------------------------------- /src/services/errorMonitoring.service.ts: -------------------------------------------------------------------------------- 1 | import { logger } from "@/config/logger"; 2 | import { AppError, isAppError } from "@/utils/appError"; 3 | import { ErrorCode } from "@/utils/errorCodes"; 4 | import { ENV } from "@/config/env"; 5 | 6 | export class ErrorMonitoringService { 7 | private static instance: ErrorMonitoringService; 8 | 9 | private constructor() { 10 | process.on("uncaughtException", this.handleUncaughtException); 11 | process.on("unhandledRejection", this.handleUnhandledRejection); 12 | } 13 | 14 | public static getInstance(): ErrorMonitoringService { 15 | if (!ErrorMonitoringService.instance) { 16 | ErrorMonitoringService.instance = new ErrorMonitoringService(); 17 | } 18 | return ErrorMonitoringService.instance; 19 | } 20 | 21 | public logError(error: Error | AppError, request?: any) { 22 | const errorLog = this.formatError(error, request); 23 | 24 | if (isAppError(error) && error.isOperational) { 25 | logger.warn(errorLog); 26 | } else { 27 | logger.error(errorLog); 28 | } 29 | 30 | // Here you could add integration with external error monitoring services 31 | // like Sentry, New Relic, etc. 32 | } 33 | 34 | private formatError(error: Error | AppError, request?: any) { 35 | const baseError = { 36 | message: error.message, 37 | stack: ENV.NODE_ENV === "development" ? error.stack : undefined, 38 | timestamp: new Date().toISOString(), 39 | }; 40 | 41 | if (isAppError(error)) { 42 | return { 43 | ...baseError, 44 | code: error.code, 45 | statusCode: error.statusCode, 46 | isOperational: error.isOperational, 47 | details: error.details, 48 | request: request 49 | ? { 50 | url: request.url, 51 | method: request.method, 52 | params: request.params, 53 | query: request.query, 54 | body: request.body, 55 | userId: request.user?.id, 56 | } 57 | : undefined, 58 | }; 59 | } 60 | 61 | return baseError; 62 | } 63 | 64 | private handleUncaughtException = (error: Error) => { 65 | logger.error("UNCAUGHT EXCEPTION! Shutting down...", { 66 | error: this.formatError(error), 67 | }); 68 | process.exit(1); 69 | }; 70 | 71 | private handleUnhandledRejection = (reason: any) => { 72 | logger.error("UNHANDLED REJECTION! Shutting down...", { 73 | error: this.formatError( 74 | reason instanceof Error ? reason : new Error(String(reason)) 75 | ), 76 | }); 77 | process.exit(1); 78 | }; 79 | } 80 | 81 | export const errorMonitoring = ErrorMonitoringService.getInstance(); 82 | -------------------------------------------------------------------------------- /src/services/metrics.service.ts: -------------------------------------------------------------------------------- 1 | import client from "prom-client"; 2 | import { logger } from "@/config/logger"; 3 | import { singleton } from "@/decorators/singleton"; 4 | import { performance, PerformanceObserver } from 'perf_hooks'; 5 | 6 | interface GCPerformanceEntry extends PerformanceEntry { 7 | detail?: { 8 | kind: string; 9 | }; 10 | } 11 | 12 | @singleton 13 | export class MetricsService { 14 | private register!: client.Registry; 15 | private httpRequestDuration!: client.Histogram; 16 | private httpRequestTotal!: client.Counter; 17 | private activeUsers!: client.Gauge; 18 | private dbQueryDuration!: client.Histogram; 19 | private websocketConnections!: client.Gauge; 20 | private websocketMessages!: client.Counter; 21 | private apiLatencyPercentiles!: client.Summary; 22 | private circuitBreakerState!: client.Gauge; 23 | private httpErrors!: client.Counter; 24 | private nodeProcessStats!: client.Gauge; 25 | private gcStats!: client.Histogram; 26 | 27 | constructor() { 28 | this.register = new client.Registry(); 29 | this.initializeMetrics(); 30 | } 31 | 32 | private initializeMetrics(): void { 33 | try { 34 | // Initialize default metrics with error handling (only once) 35 | client.collectDefaultMetrics({ 36 | register: this.register, 37 | prefix: 'node_', 38 | labels: { service: 'express-api' }, 39 | gcDurationBuckets: [0.001, 0.01, 0.1, 1, 2, 5] 40 | }); 41 | 42 | // Initialize custom metrics 43 | this.initializeHttpMetrics(); 44 | this.initializeBusinessMetrics(); 45 | this.initializeDatabaseMetrics(); 46 | this.initializeWebsocketMetrics(); 47 | this.initializeNodeMetrics(); 48 | } catch (error) { 49 | logger.error('Failed to initialize metrics', { error }); 50 | throw error; 51 | } 52 | } 53 | 54 | private initializeHttpMetrics(): void { 55 | this.httpRequestDuration = new client.Histogram({ 56 | name: "http_request_duration_seconds", 57 | help: "Duration of HTTP requests in seconds", 58 | labelNames: ["method", "route", "status_code"], 59 | buckets: [0.1, 0.3, 0.5, 0.7, 1, 3, 5, 7, 10], 60 | registers: [this.register] 61 | }); 62 | 63 | this.httpRequestTotal = new client.Counter({ 64 | name: "http_requests_total", 65 | help: "Total number of HTTP requests", 66 | labelNames: ["method", "route", "status_code"], 67 | registers: [this.register] 68 | }); 69 | 70 | this.apiLatencyPercentiles = new client.Summary({ 71 | name: 'http_request_duration_percentiles', 72 | help: 'HTTP request latency percentiles', 73 | percentiles: [0.5, 0.9, 0.95, 0.99], 74 | labelNames: ['method', 'route'], 75 | registers: [this.register] 76 | }); 77 | 78 | this.httpErrors = new client.Counter({ 79 | name: "http_errors_total", 80 | help: "Total number of HTTP errors", 81 | labelNames: ["method", "route", "status_code"], 82 | registers: [this.register] 83 | }); 84 | } 85 | 86 | private initializeBusinessMetrics(): void { 87 | this.activeUsers = new client.Gauge({ 88 | name: "active_users_total", 89 | help: "Number of active users", 90 | registers: [this.register] 91 | }); 92 | } 93 | 94 | private initializeDatabaseMetrics(): void { 95 | this.dbQueryDuration = new client.Histogram({ 96 | name: "db_query_duration_seconds", 97 | help: "Duration of database queries in seconds", 98 | labelNames: ["operation", "table", "success"], 99 | buckets: [0.01, 0.05, 0.1, 0.5, 1, 2], 100 | registers: [this.register] 101 | }); 102 | } 103 | 104 | private initializeWebsocketMetrics(): void { 105 | this.websocketConnections = new client.Gauge({ 106 | name: "websocket_connections_total", 107 | help: "Number of active WebSocket connections", 108 | registers: [this.register] 109 | }); 110 | 111 | this.websocketMessages = new client.Counter({ 112 | name: "websocket_messages_total", 113 | help: "Total number of WebSocket messages", 114 | labelNames: ["type", "direction"], 115 | registers: [this.register] 116 | }); 117 | } 118 | 119 | private initializeNodeMetrics(): void { 120 | // Raw Node.js metrics 121 | this.nodeProcessStats = new client.Gauge({ 122 | name: 'node_process_stats', 123 | help: 'Node.js process statistics', 124 | labelNames: ['stat'], 125 | registers: [this.register] 126 | }); 127 | 128 | // Enhanced GC metrics 129 | this.gcStats = new client.Histogram({ 130 | name: 'node_gc_duration_seconds', 131 | help: 'Garbage collection duration by type', 132 | labelNames: ['gc_type'], 133 | buckets: [0.001, 0.01, 0.1, 1, 2, 5], 134 | registers: [this.register] 135 | }); 136 | 137 | // Setup GC Performance Observer 138 | try { 139 | const obs = new PerformanceObserver((list) => { 140 | const entries = list.getEntries() as GCPerformanceEntry[]; 141 | 142 | entries.forEach((entry) => { 143 | const gcType = entry.detail?.kind ?? 'unknown'; 144 | const duration = entry.duration / 1000; // Convert to seconds 145 | 146 | this.gcStats.observe({ gc_type: this.getGCType(gcType) }, duration); 147 | 148 | logger.debug('GC Event recorded', { 149 | type: gcType, 150 | duration, 151 | startTime: entry.startTime, 152 | detail: entry.detail 153 | }); 154 | }); 155 | }); 156 | 157 | obs.observe({ entryTypes: ['gc'] }); 158 | logger.info('GC monitoring enabled via performance hooks'); 159 | } catch (error) { 160 | logger.warn('GC monitoring could not be enabled', { error }); 161 | } 162 | } 163 | 164 | private getGCType(type: string): string { 165 | switch (type) { 166 | case 'minor': 167 | return 'Scavenge'; 168 | case 'major': 169 | return 'MarkSweepCompact'; 170 | case 'incremental': 171 | return 'IncrementalMarking'; 172 | case 'weakcb': 173 | return 'WeakPhantomCallbackProcessing'; 174 | default: 175 | return type; 176 | } 177 | } 178 | 179 | private initializeSystemMetrics(): void { 180 | try { 181 | logger.info('System metrics initialized', { 182 | metrics: this.register.getMetricsAsJSON() 183 | }); 184 | } catch (error) { 185 | logger.error('Failed to initialize system metrics', { error }); 186 | throw error; 187 | } 188 | } 189 | 190 | // Public methods for recording metrics 191 | public recordHttpRequest(method: string, route: string, statusCode: number, duration: number): void { 192 | const labels = { method, route, status_code: statusCode.toString() }; 193 | this.httpRequestDuration.observe(labels, duration); 194 | this.httpRequestTotal.inc(labels); 195 | this.apiLatencyPercentiles.observe({ method, route }, duration); 196 | } 197 | 198 | public recordDbQuery(operation: string, table: string, duration: number, success: boolean): void { 199 | this.dbQueryDuration.observe( 200 | { operation, table, success: success.toString() }, 201 | duration 202 | ); 203 | } 204 | 205 | public updateActiveUsers(count: number): void { 206 | this.activeUsers.set(count); 207 | } 208 | 209 | public recordWebsocketConnection(isConnect: boolean): void { 210 | if (isConnect) { 211 | this.websocketConnections.inc(); 212 | } else { 213 | this.websocketConnections.dec(); 214 | } 215 | } 216 | 217 | public recordWebsocketMessage(type: string, direction: 'in' | 'out'): void { 218 | this.websocketMessages.inc({ type, direction }); 219 | } 220 | 221 | public updateCircuitBreakerState(service: string, state: 'closed' | 'open' | 'half-open'): void { 222 | const stateValue = state === 'closed' ? 0 : state === 'open' ? 1 : 0.5; 223 | this.circuitBreakerState.set({ service }, stateValue); 224 | } 225 | 226 | public async getMetrics(): Promise { 227 | try { 228 | const metrics = await this.register.metrics(); 229 | logger.debug('Metrics requested', { 230 | metricsLength: metrics.length, 231 | sampleMetrics: metrics.slice(0, 200) // Log first 200 chars for debugging 232 | }); 233 | return metrics; 234 | } catch (error) { 235 | logger.error('Error generating metrics', { error }); 236 | throw error; 237 | } 238 | } 239 | 240 | public getContentType(): string { 241 | return this.register.contentType; 242 | } 243 | 244 | public recordHttpError(method: string, route: string, statusCode: number): void { 245 | this.httpErrors.labels(method, route, statusCode.toString()).inc(); 246 | } 247 | 248 | public updateNodeStats(): void { 249 | const stats = process.memoryUsage(); 250 | this.nodeProcessStats.set({ stat: 'heap_used' }, stats.heapUsed); 251 | this.nodeProcessStats.set({ stat: 'heap_total' }, stats.heapTotal); 252 | this.nodeProcessStats.set({ stat: 'rss' }, stats.rss); 253 | this.nodeProcessStats.set({ stat: 'external' }, stats.external); 254 | 255 | const cpuUsage = process.cpuUsage(); 256 | this.nodeProcessStats.set({ stat: 'cpu_user' }, cpuUsage.user); 257 | this.nodeProcessStats.set({ stat: 'cpu_system' }, cpuUsage.system); 258 | } 259 | 260 | public recordGCStats(type: string, duration: number): void { 261 | this.gcStats.observe({ gc_type: type }, duration); 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /src/services/user.service.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | import { AppError } from "@/utils/appError"; 3 | 4 | const prisma = new PrismaClient(); 5 | 6 | export class UserService { 7 | async getAllUsers(page = 1, limit = 10) { 8 | const skip = (page - 1) * limit; 9 | return await prisma.user.findMany({ 10 | take: limit, 11 | skip, 12 | select: { 13 | id: true, 14 | name: true, 15 | email: true, 16 | role: true, 17 | createdAt: true, 18 | updatedAt: true, 19 | }, 20 | }); 21 | } 22 | 23 | async getUserById(id: string) { 24 | const user = await prisma.user.findUnique({ 25 | where: { id }, 26 | select: { 27 | id: true, 28 | name: true, 29 | email: true, 30 | role: true, 31 | createdAt: true, 32 | updatedAt: true, 33 | }, 34 | }); 35 | 36 | if (!user) { 37 | throw new AppError("User not found", 404); 38 | } 39 | 40 | return user; 41 | } 42 | 43 | async updateUser( 44 | id: string, 45 | data: Partial<{ 46 | name: string; 47 | email: string; 48 | role: "ADMIN" | "USER"; 49 | }> 50 | ) { 51 | return prisma.user.update({ 52 | where: { id }, 53 | data, 54 | select: { 55 | id: true, 56 | name: true, 57 | email: true, 58 | role: true, 59 | createdAt: true, 60 | updatedAt: true, 61 | }, 62 | }); 63 | } 64 | 65 | async deleteUser(id: string) { 66 | await prisma.user.delete({ 67 | where: { id }, 68 | }); 69 | } 70 | 71 | async createUser(data: { 72 | name: string; 73 | email: string; 74 | password: string; 75 | role?: "ADMIN" | "USER"; 76 | }) { 77 | return prisma.user.create({ 78 | data, 79 | select: { 80 | id: true, 81 | name: true, 82 | email: true, 83 | role: true, 84 | createdAt: true, 85 | updatedAt: true, 86 | }, 87 | }); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/services/websocket.service.ts: -------------------------------------------------------------------------------- 1 | import { Server } from 'http'; 2 | import { WebSocketServer, WebSocket } from 'ws'; 3 | import { ErrorMonitoringService } from './errorMonitoring.service'; 4 | import { MetricsService } from '@/services/metrics.service'; 5 | import { singleton } from '@/decorators/singleton'; 6 | 7 | export interface WebSocketMessage { 8 | type: 'ping' | 'pong' | 'error' | 'connection'; 9 | data: unknown; 10 | } 11 | 12 | @singleton 13 | export class WebSocketService { 14 | private static instance: WebSocketService; 15 | private wss!: WebSocketServer; 16 | private metricsService: MetricsService; 17 | 18 | constructor() { 19 | this.metricsService = new MetricsService(); 20 | } 21 | 22 | public static getInstance(server?: Server): WebSocketService { 23 | if (!WebSocketService.instance) { 24 | WebSocketService.instance = new WebSocketService(); 25 | if (server) { 26 | WebSocketService.instance.initialize(server); 27 | } 28 | } 29 | return WebSocketService.instance; 30 | } 31 | 32 | private initialize(server: Server): void { 33 | this.wss = new WebSocketServer({ server }); 34 | 35 | this.wss.on('connection', (ws: WebSocket) => { 36 | this.metricsService.recordWebsocketConnection(true); 37 | 38 | ws.on('close', () => { 39 | this.metricsService.recordWebsocketConnection(false); 40 | }); 41 | 42 | ws.on('message', (message: string) => { 43 | this.metricsService.recordWebsocketMessage('message', 'in'); 44 | }); 45 | }); 46 | } 47 | 48 | // Rest of the WebSocket service implementation... 49 | } -------------------------------------------------------------------------------- /src/templates/emails/index.ts: -------------------------------------------------------------------------------- 1 | export * from './verification.template'; 2 | export * from './reset-password.template'; 3 | // Export other email templates here as they are added -------------------------------------------------------------------------------- /src/templates/emails/reset-password.template.ts: -------------------------------------------------------------------------------- 1 | export const getPasswordResetEmailTemplate = (name: string, resetUrl: string) => ` 2 | 3 | 4 | 5 | 6 | 7 | Password Reset 8 | 32 | 33 | 34 |

Hello ${name},

35 |

You requested to reset your password. Click the button below to reset it:

36 | 37 | Reset Password 38 | 39 |

Or copy and paste this link in your browser:

40 |

${resetUrl}

41 | 42 |

This link will expire in 1 hour.

43 | 44 | 48 | 49 | 50 | `; -------------------------------------------------------------------------------- /src/templates/emails/verification.template.ts: -------------------------------------------------------------------------------- 1 | export const getVerificationEmailTemplate = (name: string, verificationUrl: string) => ` 2 | 3 | 4 | 5 | 6 | 7 | Email Verification 8 | 32 | 33 | 34 |

Hello ${name},

35 |

Thank you for signing up! Please verify your email address by clicking the button below:

36 | 37 | Verify Email 38 | 39 |

Or copy and paste this link in your browser:

40 |

${verificationUrl}

41 | 42 |

This link will expire in 24 hours.

43 | 44 | 48 | 49 | 50 | `; -------------------------------------------------------------------------------- /src/utils/apiResponse.ts: -------------------------------------------------------------------------------- 1 | import { Response } from "express"; 2 | 3 | export class ApiResponse { 4 | static success(res: Response, data: any = null, message: string = "Success"): void { 5 | res.status(200).json({ 6 | success: true, 7 | message, 8 | data, 9 | }); 10 | } 11 | 12 | static error(res: Response, message: string, statusCode: number = 400, code?: string): void { 13 | res.status(statusCode).json({ 14 | success: false, 15 | message, 16 | code, 17 | ...(process.env.NODE_ENV === 'development' && { stack: new Error().stack }) 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/utils/appError.ts: -------------------------------------------------------------------------------- 1 | import { ErrorCode } from "./errorCodes"; 2 | 3 | export class AppError extends Error { 4 | public readonly statusCode: number; 5 | public readonly code: ErrorCode; 6 | public readonly isOperational: boolean; 7 | public readonly details?: any; 8 | 9 | constructor( 10 | message: string, 11 | statusCode: number = 500, 12 | code: ErrorCode = ErrorCode.INTERNAL_SERVER_ERROR, 13 | isOperational: boolean = true, 14 | details?: any 15 | ) { 16 | super(message); 17 | this.statusCode = statusCode; 18 | this.code = code; 19 | this.isOperational = isOperational; 20 | this.details = details; 21 | 22 | Error.captureStackTrace(this, this.constructor); 23 | } 24 | } 25 | 26 | export const isAppError = (error: any): error is AppError => { 27 | return error instanceof AppError; 28 | }; 29 | -------------------------------------------------------------------------------- /src/utils/errorCodes.ts: -------------------------------------------------------------------------------- 1 | export enum ErrorCode { 2 | // Authentication Errors (1xxx) 3 | UNAUTHORIZED = "ERR_1001", 4 | INVALID_CREDENTIALS = "ERR_1002", 5 | TOKEN_EXPIRED = "ERR_1003", 6 | INVALID_TOKEN = "ERR_1004", 7 | 8 | // Authorization Errors (2xxx) 9 | FORBIDDEN = "ERR_2001", 10 | INSUFFICIENT_PERMISSIONS = "ERR_2002", 11 | 12 | // Validation Errors (3xxx) 13 | INVALID_INPUT = "ERR_3001", 14 | MISSING_REQUIRED_FIELD = "ERR_3002", 15 | INVALID_EMAIL = "ERR_3003", 16 | INVALID_PASSWORD = "ERR_3004", 17 | INVALID_REQUEST = "ERR_3005", 18 | 19 | // Resource Errors (4xxx) 20 | NOT_FOUND = "ERR_4001", 21 | ALREADY_EXISTS = "ERR_4002", 22 | CONFLICT = "ERR_4003", 23 | 24 | // Database Errors (5xxx) 25 | DB_ERROR = "ERR_5001", 26 | DB_CONNECTION_ERROR = "ERR_5002", 27 | DB_QUERY_ERROR = "ERR_5003", 28 | 29 | // Server Errors (6xxx) 30 | INTERNAL_SERVER_ERROR = "ERR_6001", 31 | SERVICE_UNAVAILABLE = "ERR_6002", 32 | EXTERNAL_SERVICE_ERROR = "ERR_6003", 33 | 34 | // Validation Errors (3xxx) 35 | VALIDATION_ERROR = 'ERR_3006', 36 | } 37 | -------------------------------------------------------------------------------- /src/utils/errorHandler.ts: -------------------------------------------------------------------------------- 1 | import { AppError } from "./appError"; 2 | import { logger } from "@/config/logger"; 3 | import { ErrorCode } from "./errorCodes"; 4 | import { Prisma } from "@prisma/client"; 5 | 6 | export class ErrorHandler { 7 | static handle(error: unknown, context: string) { 8 | // Handle Prisma errors 9 | if (error instanceof Prisma.PrismaClientKnownRequestError) { 10 | const prismaError = this.handlePrismaError(error); 11 | logger.warn("Prisma error occurred", { 12 | message: prismaError.message, 13 | context, 14 | code: prismaError.code, 15 | statusCode: prismaError.statusCode, 16 | prismaCode: error.code, 17 | meta: error.meta, // Include Prisma metadata 18 | }); 19 | return prismaError; 20 | } 21 | 22 | if (error instanceof AppError) { 23 | logger.warn("Application error occurred", { 24 | message: error.message, 25 | context, 26 | code: error.code, 27 | statusCode: error.statusCode, 28 | details: error.details, 29 | stack: error.stack, 30 | }); 31 | return error; 32 | } 33 | 34 | // Log unknown errors 35 | const unknownError = new AppError( 36 | "Internal server error", 37 | 500, 38 | ErrorCode.INTERNAL_SERVER_ERROR, 39 | false 40 | ); 41 | 42 | logger.error("Unknown error occurred", { 43 | message: error instanceof Error ? error.message : "Unknown error", 44 | context, 45 | error: error instanceof Error ? error.stack : JSON.stringify(error), 46 | details: error instanceof Error ? error : undefined, 47 | }); 48 | 49 | return unknownError; 50 | } 51 | 52 | private static handlePrismaError( 53 | error: Prisma.PrismaClientKnownRequestError 54 | ): AppError { 55 | switch (error.code) { 56 | case "P2002": 57 | return new AppError( 58 | "Resource already exists", 59 | 409, 60 | ErrorCode.ALREADY_EXISTS 61 | ); 62 | case "P2025": 63 | return new AppError("Resource not found", 404, ErrorCode.NOT_FOUND); 64 | default: 65 | return new AppError("Database error", 500, ErrorCode.DB_ERROR, false); 66 | } 67 | } 68 | } 69 | 70 | // Add more specific error types 71 | export class ValidationError extends AppError { 72 | constructor(message: string) { 73 | super(message, 400, ErrorCode.VALIDATION_ERROR); 74 | } 75 | } 76 | 77 | export class DatabaseError extends AppError { 78 | constructor(message: string) { 79 | super(message, 500, ErrorCode.DB_ERROR); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/validators/auth.validator.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const signupSchema = z.object({ 4 | body: z.object({ 5 | name: z.string().min(2).max(99), 6 | email: z.string().email().max(99), 7 | password: z 8 | .string() 9 | .min(8) 10 | .max(100) 11 | .regex( 12 | /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/, 13 | "Password must contain at least one uppercase letter, one lowercase letter, one number and one special character" 14 | ), 15 | }), 16 | }); 17 | 18 | export const loginSchema = z.object({ 19 | body: z.object({ 20 | email: z.string().email(), 21 | password: z.string().min(6), 22 | }), 23 | }); 24 | 25 | export const refreshTokenSchema = z.object({ 26 | body: z.object({ 27 | refreshToken: z.string().min(1, "Refresh token is required"), 28 | }), 29 | }); 30 | 31 | export const resendVerificationSchema = z.object({ 32 | body: z.object({ 33 | email: z.string().email("Invalid email address"), 34 | }), 35 | }); 36 | 37 | export const verifyEmailSchema = z.object({ 38 | params: z.object({ 39 | token: z.string().min(1, "Verification token is required"), 40 | }), 41 | }); 42 | 43 | export const forgotPasswordSchema = z.object({ 44 | body: z.object({ 45 | email: z.string().email("Invalid email address"), 46 | }), 47 | }); 48 | 49 | export const resetPasswordSchema = z.object({ 50 | params: z.object({ 51 | token: z.string().min(1, "Reset token is required"), 52 | }), 53 | body: z.object({ 54 | password: z 55 | .string() 56 | .min(8) 57 | .max(100) 58 | .regex( 59 | /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/, 60 | "Password must contain at least one uppercase letter, one lowercase letter, one number and one special character" 61 | ), 62 | }), 63 | }); 64 | -------------------------------------------------------------------------------- /src/validators/user.validator.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const createUserSchema = z.object({ 4 | body: z.object({ 5 | name: z.string().min(2).max(99), 6 | email: z.string().email().max(99), 7 | password: z.string().min(8).max(100), 8 | role: z.enum(["ADMIN", "USER"]).optional(), 9 | }), 10 | }); 11 | 12 | export const updateUserSchema = z.object({ 13 | body: z.object({ 14 | name: z.string().min(2).max(99).optional(), 15 | email: z.string().email().max(99).optional(), 16 | role: z.enum(["ADMIN", "USER"]).optional(), 17 | }), 18 | params: z.object({ 19 | id: z.string().uuid(), 20 | }), 21 | }); 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es6" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 22 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | 26 | /* Modules */ 27 | "module": "commonjs" /* Specify what module code is generated. */, 28 | "rootDir": "./" /* Specify the root folder within your source files. */, 29 | "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, 30 | "baseUrl": ".", 31 | "paths": { 32 | "@/*": ["./src/*"], 33 | "@templates/*": ["src/templates/*"] 34 | }, 35 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 36 | "typeRoots": ["./src/@types", "./node_modules/@types"], 37 | "types": ["node", "jest"], 38 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 39 | "resolveJsonModule": true /* Enable importing .json files */, 40 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 41 | 42 | /* JavaScript Support */ 43 | "allowJs": true, 44 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 45 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 46 | 47 | /* Emit */ 48 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 49 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 50 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 51 | "sourceMap": true, 52 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 53 | "outDir": "./dist", 54 | // "removeComments": true, /* Disable emitting comments. */ 55 | // "noEmit": true, /* Disable emitting files from a compilation. */ 56 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 57 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 58 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 59 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 60 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 61 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 62 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 63 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 64 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 65 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 66 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 67 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 68 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 69 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 70 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 71 | 72 | /* Interop Constraints */ 73 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 74 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 75 | "esModuleInterop": true, 76 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 77 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 78 | 79 | /* Type Checking */ 80 | "strict": true /* Enable all strict type-checking options. */, 81 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ 82 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 83 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 84 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 85 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 86 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 87 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 88 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 89 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 90 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 91 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 92 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 93 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 94 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 95 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 96 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 97 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 98 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 99 | 100 | /* Completeness */ 101 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 102 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 103 | }, 104 | "ts-node": { 105 | "transpileOnly": true, 106 | "require": ["tsconfig-paths/register"], 107 | "files": true 108 | }, 109 | "include": ["src/**/*"], 110 | "exclude": ["node_modules"] 111 | } 112 | -------------------------------------------------------------------------------- /tsconfig.scripts.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "outDir": "dist" 6 | }, 7 | "include": ["scripts/**/*"] 8 | } 9 | --------------------------------------------------------------------------------