diff --git a/.github/workflows/api-docs.yml b/.github/workflows/api-docs.yml
new file mode 100644
index 00000000..1b2e7d67
--- /dev/null
+++ b/.github/workflows/api-docs.yml
@@ -0,0 +1,50 @@
+name: API Docs
+
+on:
+ push:
+ branches: [main]
+ paths:
+ - 'src/**/*.ts'
+ - 'scripts/generate-api-docs.js'
+ - 'package.json'
+ - 'package-lock.json'
+ - 'docs/api/**'
+ - '.github/workflows/api-docs.yml'
+ workflow_dispatch:
+
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+concurrency:
+ group: api-docs-pages
+ cancel-in-progress: true
+
+jobs:
+ build:
+ name: Generate API Docs
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+ cache: npm
+ - run: npm ci
+ - run: npm run docs:generate
+ - uses: actions/configure-pages@v5
+ - uses: actions/upload-pages-artifact@v3
+ with:
+ path: docs/site
+
+ deploy:
+ name: Deploy API Docs
+ runs-on: ubuntu-latest
+ needs: build
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ steps:
+ - id: deployment
+ uses: actions/deploy-pages@v4
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index efb0b332..39f5eb95 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -89,6 +89,27 @@ jobs:
- uses: actions/upload-artifact@v4
with: { name: dist, path: dist/, retention-days: 1 }
+ api-docs:
+ name: API Documentation
+ runs-on: ubuntu-latest
+ needs: install
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with: { node-version: '${{ env.NODE_VERSION }}', cache: 'npm' }
+ - uses: actions/cache@v4
+ with:
+ path: node_modules
+ key: node-modules-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
+ - run: npm run docs:generate
+ - name: Verify generated API docs are committed
+ run: git diff --exit-code -- openapi-spec.json docs/api/openapi-spec.json docs/api/examples.md docs/site
+ - uses: actions/upload-artifact@v4
+ with:
+ name: api-docs-site
+ path: docs/site/
+ retention-days: 7
+
unit-tests:
name: Unit Tests & Coverage
runs-on: ubuntu-latest
@@ -180,7 +201,7 @@ jobs:
ci-success:
name: CI Passed
runs-on: ubuntu-latest
- needs: [install, lint, format, typecheck, build, unit-tests, e2e-tests]
+ needs: [install, lint, format, typecheck, build, api-docs, unit-tests, e2e-tests]
if: always()
steps:
- name: Check all jobs passed
diff --git a/docs/api/README.md b/docs/api/README.md
index f4c23808..704133f0 100644
--- a/docs/api/README.md
+++ b/docs/api/README.md
@@ -27,6 +27,17 @@ Welcome to the comprehensive API documentation for the TeachLink platform. This
**Interactive Documentation**:
- Swagger UI: http://localhost:3000/api/docs
+**Generated Artifacts**:
+- OpenAPI JSON: [openapi-spec.json](./openapi-spec.json)
+- Example requests/responses: [examples.md](./examples.md)
+- Static docs site source: [../site/index.html](../site/index.html)
+
+Regenerate all API documentation artifacts with:
+
+```bash
+npm run docs:generate
+```
+
## Authentication
Most API endpoints require authentication using JWT (JSON Web Tokens).
diff --git a/docs/api/examples.md b/docs/api/examples.md
new file mode 100644
index 00000000..74b4de93
--- /dev/null
+++ b/docs/api/examples.md
@@ -0,0 +1,84 @@
+# API Examples
+
+This file is generated by `npm run docs:generate` from `scripts/generate-api-docs.js`.
+
+## Login
+
+```bash
+curl -X POST http://localhost:3000/auth/login \
+ -H "Content-Type: application/json" \
+ -d '{"email":"learner@example.com","password":"Password123!"}'
+```
+
+```json
+{
+ "success": true,
+ "message": "Login successful",
+ "data": {
+ "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
+ "refreshToken": "refresh_01JZ0D4R8R2Y3R9H2W6E5R4T1P",
+ "user": {
+ "id": "2f4d8b5f-91d2-43a1-bd1e-877b4f97d7b9",
+ "email": "learner@example.com",
+ "firstName": "Ada",
+ "lastName": "Lovelace",
+ "role": "student",
+ "status": "active"
+ }
+ }
+}
+```
+
+## Create Course
+
+```bash
+curl -X POST http://localhost:3000/courses \
+ -H "Authorization: Bearer YOUR_TOKEN" \
+ -H "Content-Type: application/json" \
+ -d '{"title":"JavaScript Foundations","description":"Learn modern JavaScript from first principles.","category":"programming","level":"beginner","price":3999}'
+```
+
+```json
+{
+ "success": true,
+ "message": "Course created",
+ "data": {
+ "id": "8e4fd4f8-d8f3-46b5-8786-6f7167a654f4",
+ "title": "JavaScript Foundations",
+ "description": "Learn modern JavaScript from first principles.",
+ "category": "programming",
+ "level": "beginner",
+ "price": 3999,
+ "status": "published"
+ }
+}
+```
+
+## Search Content
+
+```bash
+curl "http://localhost:3000/search?q=javascript%20basics&filters=%7B%22category%22%3A%22programming%22%7D"
+```
+
+```json
+{
+ "results": [
+ {
+ "id": "8e4fd4f8-d8f3-46b5-8786-6f7167a654f4",
+ "title": "JavaScript Foundations",
+ "description": "Learn modern JavaScript from first principles.",
+ "category": "programming",
+ "level": "beginner",
+ "price": 3999,
+ "status": "published"
+ }
+ ],
+ "total": 1,
+ "page": 1,
+ "limit": 20,
+ "filters": {
+ "category": "programming"
+ },
+ "query": "javascript basics"
+}
+```
diff --git a/docs/api/openapi-spec.json b/docs/api/openapi-spec.json
new file mode 100644
index 00000000..356525f5
--- /dev/null
+++ b/docs/api/openapi-spec.json
@@ -0,0 +1,1097 @@
+{
+ "openapi": "3.0.3",
+ "info": {
+ "title": "TeachLink API",
+ "description": "Automatically generated OpenAPI documentation for TeachLink backend APIs, including request and response examples.",
+ "version": "0.0.1"
+ },
+ "servers": [
+ {
+ "url": "http://localhost:3000",
+ "description": "Local development"
+ },
+ {
+ "url": "https://api.teachlink.com",
+ "description": "Production"
+ }
+ ],
+ "tags": [
+ {
+ "name": "App",
+ "description": "Service metadata and status"
+ },
+ {
+ "name": "Auth",
+ "description": "Registration, login, and token management"
+ },
+ {
+ "name": "Users",
+ "description": "User account management"
+ },
+ {
+ "name": "Courses",
+ "description": "Course catalog and authoring"
+ },
+ {
+ "name": "Payments",
+ "description": "Payments, subscriptions, and refunds"
+ },
+ {
+ "name": "Search",
+ "description": "Search, filters, autocomplete, and analytics"
+ },
+ {
+ "name": "Debugging",
+ "description": "Admin-only request capture and replay tools"
+ }
+ ],
+ "paths": {
+ "/": {
+ "get": {
+ "tags": [
+ "App"
+ ],
+ "summary": "Get app status",
+ "operationId": "getAppStatus",
+ "responses": {
+ "200": {
+ "description": "App is running",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiSuccess"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "message": "TeachLink API is running",
+ "timestamp": "2026-05-27T18:00:00.000Z"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/auth/register": {
+ "post": {
+ "tags": [
+ "Auth"
+ ],
+ "summary": "Register a new user",
+ "operationId": "registerUser",
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/RegisterRequest"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "email": "learner@example.com",
+ "password": "Password123!",
+ "firstName": "Ada",
+ "lastName": "Lovelace",
+ "role": "student"
+ }
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "201": {
+ "description": "Registration successful",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiSuccess"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "success": true,
+ "message": "Registration successful",
+ "data": {
+ "id": "2f4d8b5f-91d2-43a1-bd1e-877b4f97d7b9",
+ "email": "learner@example.com",
+ "firstName": "Ada",
+ "lastName": "Lovelace",
+ "role": "student",
+ "status": "active"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid registration data",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiError"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "success": false,
+ "message": "Validation failed",
+ "errors": [
+ {
+ "field": "email",
+ "message": "Validation failed"
+ }
+ ]
+ }
+ }
+ }
+ }
+ }
+ },
+ "409": {
+ "description": "Email already exists",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiError"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "success": false,
+ "message": "Email already exists",
+ "errors": []
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/auth/login": {
+ "post": {
+ "tags": [
+ "Auth"
+ ],
+ "summary": "Log in with email and password",
+ "operationId": "loginUser",
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/LoginRequest"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "email": "learner@example.com",
+ "password": "Password123!"
+ }
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Login successful",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiSuccess"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "success": true,
+ "message": "Login successful",
+ "data": {
+ "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
+ "refreshToken": "refresh_01JZ0D4R8R2Y3R9H2W6E5R4T1P",
+ "user": {
+ "id": "2f4d8b5f-91d2-43a1-bd1e-877b4f97d7b9",
+ "email": "learner@example.com",
+ "firstName": "Ada",
+ "lastName": "Lovelace",
+ "role": "student",
+ "status": "active"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Invalid credentials",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiError"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "success": false,
+ "message": "Invalid credentials",
+ "errors": []
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/users": {
+ "get": {
+ "tags": [
+ "Users"
+ ],
+ "summary": "List users",
+ "operationId": "listUsers",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "page",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "default": 1
+ }
+ },
+ {
+ "name": "limit",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "default": 20,
+ "maximum": 100
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Users found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiSuccess"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "success": true,
+ "message": "Operation completed successfully",
+ "data": [
+ {
+ "id": "2f4d8b5f-91d2-43a1-bd1e-877b4f97d7b9",
+ "email": "learner@example.com",
+ "firstName": "Ada",
+ "lastName": "Lovelace",
+ "role": "student",
+ "status": "active"
+ }
+ ]
+ }
+ }
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Authentication required",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiError"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "success": false,
+ "message": "Authentication required",
+ "errors": []
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "post": {
+ "tags": [
+ "Users"
+ ],
+ "summary": "Create a user",
+ "operationId": "createUser",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/RegisterRequest"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "email": "teacher@example.com",
+ "password": "Password123!",
+ "firstName": "Grace",
+ "lastName": "Hopper",
+ "role": "teacher"
+ }
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "201": {
+ "description": "User created",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiSuccess"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "success": true,
+ "message": "User created",
+ "data": {
+ "id": "2f4d8b5f-91d2-43a1-bd1e-877b4f97d7b9",
+ "email": "learner@example.com",
+ "firstName": "Ada",
+ "lastName": "Lovelace",
+ "role": "teacher",
+ "status": "active"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid user data",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiError"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "success": false,
+ "message": "Validation failed",
+ "errors": []
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/courses": {
+ "get": {
+ "tags": [
+ "Courses"
+ ],
+ "summary": "List courses",
+ "operationId": "listCourses",
+ "parameters": [
+ {
+ "name": "page",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "default": 1
+ }
+ },
+ {
+ "name": "limit",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "default": 20,
+ "maximum": 100
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Courses found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiSuccess"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "success": true,
+ "message": "Operation completed successfully",
+ "data": [
+ {
+ "id": "8e4fd4f8-d8f3-46b5-8786-6f7167a654f4",
+ "title": "JavaScript Foundations",
+ "description": "Learn modern JavaScript from first principles.",
+ "category": "programming",
+ "level": "beginner",
+ "price": 3999,
+ "status": "published"
+ }
+ ]
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "post": {
+ "tags": [
+ "Courses"
+ ],
+ "summary": "Create a course",
+ "operationId": "createCourse",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/CourseRequest"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "title": "JavaScript Foundations",
+ "description": "Learn modern JavaScript from first principles.",
+ "category": "programming",
+ "level": "beginner",
+ "price": 3999
+ }
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "201": {
+ "description": "Course created",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiSuccess"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "success": true,
+ "message": "Course created",
+ "data": {
+ "id": "8e4fd4f8-d8f3-46b5-8786-6f7167a654f4",
+ "title": "JavaScript Foundations",
+ "description": "Learn modern JavaScript from first principles.",
+ "category": "programming",
+ "level": "beginner",
+ "price": 3999,
+ "status": "published"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid course data",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiError"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "success": false,
+ "message": "Validation failed",
+ "errors": [
+ {
+ "field": "title",
+ "message": "Validation failed"
+ }
+ ]
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/payments/create-intent": {
+ "post": {
+ "tags": [
+ "Payments"
+ ],
+ "summary": "Create a payment intent",
+ "operationId": "createPaymentIntent",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "X-Idempotency-Key",
+ "in": "header",
+ "required": false,
+ "schema": {
+ "type": "string"
+ },
+ "example": "payment-8e4fd4f8-d8f3-46b5"
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/PaymentIntentRequest"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "courseId": "8e4fd4f8-d8f3-46b5-8786-6f7167a654f4",
+ "amount": 3999,
+ "currency": "USD"
+ }
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "201": {
+ "description": "Payment intent created",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiSuccess"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "success": true,
+ "message": "Payment intent created",
+ "data": {
+ "id": "pay_01JZ0D4R8R2Y3R9H2W6E5R4T1P",
+ "amount": 3999,
+ "currency": "USD",
+ "status": "pending",
+ "providerClientSecret": "pi_123_secret_456"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "409": {
+ "description": "Duplicate idempotency key",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiError"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "success": false,
+ "message": "Request already processed",
+ "errors": []
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/search": {
+ "get": {
+ "tags": [
+ "Search"
+ ],
+ "summary": "Search courses and learning content",
+ "operationId": "searchContent",
+ "parameters": [
+ {
+ "name": "q",
+ "in": "query",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "example": "javascript basics"
+ },
+ {
+ "name": "filters",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "string"
+ },
+ "example": "{\"category\":\"programming\",\"level\":\"beginner\"}"
+ },
+ {
+ "name": "sort",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "string"
+ },
+ "example": "relevance"
+ },
+ {
+ "name": "page",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "default": 1
+ }
+ },
+ {
+ "name": "limit",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "default": 20
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Search results",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/SearchResponse"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "results": [
+ {
+ "id": "8e4fd4f8-d8f3-46b5-8786-6f7167a654f4",
+ "title": "JavaScript Foundations",
+ "description": "Learn modern JavaScript from first principles.",
+ "category": "programming",
+ "level": "beginner",
+ "price": 3999,
+ "status": "published"
+ }
+ ],
+ "total": 1,
+ "page": 1,
+ "limit": 20,
+ "filters": {
+ "category": "programming",
+ "level": "beginner"
+ },
+ "query": "javascript basics"
+ }
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid filters JSON",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiError"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "success": false,
+ "message": "filters must be valid JSON",
+ "errors": [
+ {
+ "field": "filters",
+ "message": "filters must be valid JSON"
+ }
+ ]
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/search/autocomplete": {
+ "get": {
+ "tags": [
+ "Search"
+ ],
+ "summary": "Get search autocomplete suggestions",
+ "operationId": "getAutocomplete",
+ "parameters": [
+ {
+ "name": "q",
+ "in": "query",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "example": "java"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Autocomplete suggestions",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiSuccess"
+ },
+ "examples": {
+ "default": {
+ "value": [
+ "javascript",
+ "java fundamentals",
+ "java spring"
+ ]
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/debug/requests": {
+ "get": {
+ "tags": [
+ "Debugging"
+ ],
+ "summary": "List recently captured requests",
+ "operationId": "listCapturedRequests",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "limit",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "default": 50
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Captured request summaries",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiSuccess"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "total": 1,
+ "records": [
+ {
+ "id": "req_01",
+ "timestamp": "2026-05-27T18:00:00.000Z",
+ "method": "GET",
+ "path": "/search?q=javascript",
+ "statusCode": 200,
+ "durationMs": 18,
+ "hasError": false
+ }
+ ]
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "delete": {
+ "tags": [
+ "Debugging"
+ ],
+ "summary": "Clear the captured request buffer",
+ "operationId": "clearCapturedRequests",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Capture buffer cleared",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiSuccess"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "message": "Debug capture buffer cleared"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "components": {
+ "securitySchemes": {
+ "bearerAuth": {
+ "type": "http",
+ "scheme": "bearer",
+ "bearerFormat": "JWT"
+ }
+ },
+ "schemas": {
+ "ApiSuccess": {
+ "type": "object",
+ "properties": {
+ "success": {
+ "type": "boolean",
+ "example": true
+ },
+ "message": {
+ "type": "string",
+ "example": "Operation completed successfully"
+ },
+ "data": {
+ "type": "object"
+ }
+ }
+ },
+ "ApiError": {
+ "type": "object",
+ "properties": {
+ "success": {
+ "type": "boolean",
+ "example": false
+ },
+ "message": {
+ "type": "string",
+ "example": "Validation failed"
+ },
+ "errors": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "field": {
+ "type": "string",
+ "example": "email"
+ },
+ "message": {
+ "type": "string",
+ "example": "email must be valid"
+ }
+ }
+ }
+ }
+ }
+ },
+ "LoginRequest": {
+ "type": "object",
+ "required": [
+ "email",
+ "password"
+ ],
+ "properties": {
+ "email": {
+ "type": "string",
+ "format": "email",
+ "example": "learner@example.com"
+ },
+ "password": {
+ "type": "string",
+ "format": "password",
+ "example": "Password123!"
+ }
+ }
+ },
+ "RegisterRequest": {
+ "type": "object",
+ "required": [
+ "email",
+ "password",
+ "firstName",
+ "lastName"
+ ],
+ "properties": {
+ "email": {
+ "type": "string",
+ "format": "email",
+ "example": "learner@example.com"
+ },
+ "password": {
+ "type": "string",
+ "format": "password",
+ "example": "Password123!"
+ },
+ "firstName": {
+ "type": "string",
+ "example": "Ada"
+ },
+ "lastName": {
+ "type": "string",
+ "example": "Lovelace"
+ },
+ "role": {
+ "type": "string",
+ "enum": [
+ "student",
+ "teacher"
+ ],
+ "example": "student"
+ }
+ }
+ },
+ "CourseRequest": {
+ "type": "object",
+ "required": [
+ "title",
+ "description"
+ ],
+ "properties": {
+ "title": {
+ "type": "string",
+ "example": "JavaScript Foundations"
+ },
+ "description": {
+ "type": "string",
+ "example": "Learn modern JavaScript from first principles."
+ },
+ "category": {
+ "type": "string",
+ "example": "programming"
+ },
+ "level": {
+ "type": "string",
+ "example": "beginner"
+ },
+ "price": {
+ "type": "number",
+ "example": 3999
+ }
+ }
+ },
+ "PaymentIntentRequest": {
+ "type": "object",
+ "required": [
+ "courseId",
+ "amount",
+ "currency"
+ ],
+ "properties": {
+ "courseId": {
+ "type": "string",
+ "format": "uuid",
+ "example": "8e4fd4f8-d8f3-46b5-8786-6f7167a654f4"
+ },
+ "amount": {
+ "type": "number",
+ "example": 3999
+ },
+ "currency": {
+ "type": "string",
+ "example": "USD"
+ }
+ }
+ },
+ "SearchResponse": {
+ "type": "object",
+ "properties": {
+ "results": {
+ "type": "array",
+ "items": {
+ "type": "object"
+ }
+ },
+ "total": {
+ "type": "integer",
+ "example": 1
+ },
+ "page": {
+ "type": "integer",
+ "example": 1
+ },
+ "limit": {
+ "type": "integer",
+ "example": 20
+ },
+ "filters": {
+ "type": "object"
+ },
+ "query": {
+ "type": "string",
+ "example": "javascript basics"
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/docs/site/.nojekyll b/docs/site/.nojekyll
new file mode 100644
index 00000000..e69de29b
diff --git a/docs/site/examples.md b/docs/site/examples.md
new file mode 100644
index 00000000..74b4de93
--- /dev/null
+++ b/docs/site/examples.md
@@ -0,0 +1,84 @@
+# API Examples
+
+This file is generated by `npm run docs:generate` from `scripts/generate-api-docs.js`.
+
+## Login
+
+```bash
+curl -X POST http://localhost:3000/auth/login \
+ -H "Content-Type: application/json" \
+ -d '{"email":"learner@example.com","password":"Password123!"}'
+```
+
+```json
+{
+ "success": true,
+ "message": "Login successful",
+ "data": {
+ "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
+ "refreshToken": "refresh_01JZ0D4R8R2Y3R9H2W6E5R4T1P",
+ "user": {
+ "id": "2f4d8b5f-91d2-43a1-bd1e-877b4f97d7b9",
+ "email": "learner@example.com",
+ "firstName": "Ada",
+ "lastName": "Lovelace",
+ "role": "student",
+ "status": "active"
+ }
+ }
+}
+```
+
+## Create Course
+
+```bash
+curl -X POST http://localhost:3000/courses \
+ -H "Authorization: Bearer YOUR_TOKEN" \
+ -H "Content-Type: application/json" \
+ -d '{"title":"JavaScript Foundations","description":"Learn modern JavaScript from first principles.","category":"programming","level":"beginner","price":3999}'
+```
+
+```json
+{
+ "success": true,
+ "message": "Course created",
+ "data": {
+ "id": "8e4fd4f8-d8f3-46b5-8786-6f7167a654f4",
+ "title": "JavaScript Foundations",
+ "description": "Learn modern JavaScript from first principles.",
+ "category": "programming",
+ "level": "beginner",
+ "price": 3999,
+ "status": "published"
+ }
+}
+```
+
+## Search Content
+
+```bash
+curl "http://localhost:3000/search?q=javascript%20basics&filters=%7B%22category%22%3A%22programming%22%7D"
+```
+
+```json
+{
+ "results": [
+ {
+ "id": "8e4fd4f8-d8f3-46b5-8786-6f7167a654f4",
+ "title": "JavaScript Foundations",
+ "description": "Learn modern JavaScript from first principles.",
+ "category": "programming",
+ "level": "beginner",
+ "price": 3999,
+ "status": "published"
+ }
+ ],
+ "total": 1,
+ "page": 1,
+ "limit": 20,
+ "filters": {
+ "category": "programming"
+ },
+ "query": "javascript basics"
+}
+```
diff --git a/docs/site/index.html b/docs/site/index.html
new file mode 100644
index 00000000..d2e2506c
--- /dev/null
+++ b/docs/site/index.html
@@ -0,0 +1,108 @@
+
+
+
+
+
+ TeachLink API Documentation
+
+
+
+
+
+ Generated API Docs
+ TeachLink API
+ OpenAPI 3.0.3 documentation generated from the backend documentation source.
+
+
+
+ Endpoints
+
+
+ | Method | Path | Summary | Tags |
+
+
+
+ | GET |
+ / |
+ Get app status |
+ App |
+
+
+ | POST |
+ /auth/register |
+ Register a new user |
+ Auth |
+
+
+ | POST |
+ /auth/login |
+ Log in with email and password |
+ Auth |
+
+
+ | GET |
+ /users |
+ List users |
+ Users |
+
+
+ | POST |
+ /users |
+ Create a user |
+ Users |
+
+
+ | GET |
+ /courses |
+ List courses |
+ Courses |
+
+
+ | POST |
+ /courses |
+ Create a course |
+ Courses |
+
+
+ | POST |
+ /payments/create-intent |
+ Create a payment intent |
+ Payments |
+
+
+ | GET |
+ /search |
+ Search courses and learning content |
+ Search |
+
+
+ | GET |
+ /search/autocomplete |
+ Get search autocomplete suggestions |
+ Search |
+
+
+ | GET |
+ /debug/requests |
+ List recently captured requests |
+ Debugging |
+
+
+ | DELETE |
+ /debug/requests |
+ Clear the captured request buffer |
+ Debugging |
+
+
+
+
+ Interactive Reference
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/site/openapi-spec.json b/docs/site/openapi-spec.json
new file mode 100644
index 00000000..356525f5
--- /dev/null
+++ b/docs/site/openapi-spec.json
@@ -0,0 +1,1097 @@
+{
+ "openapi": "3.0.3",
+ "info": {
+ "title": "TeachLink API",
+ "description": "Automatically generated OpenAPI documentation for TeachLink backend APIs, including request and response examples.",
+ "version": "0.0.1"
+ },
+ "servers": [
+ {
+ "url": "http://localhost:3000",
+ "description": "Local development"
+ },
+ {
+ "url": "https://api.teachlink.com",
+ "description": "Production"
+ }
+ ],
+ "tags": [
+ {
+ "name": "App",
+ "description": "Service metadata and status"
+ },
+ {
+ "name": "Auth",
+ "description": "Registration, login, and token management"
+ },
+ {
+ "name": "Users",
+ "description": "User account management"
+ },
+ {
+ "name": "Courses",
+ "description": "Course catalog and authoring"
+ },
+ {
+ "name": "Payments",
+ "description": "Payments, subscriptions, and refunds"
+ },
+ {
+ "name": "Search",
+ "description": "Search, filters, autocomplete, and analytics"
+ },
+ {
+ "name": "Debugging",
+ "description": "Admin-only request capture and replay tools"
+ }
+ ],
+ "paths": {
+ "/": {
+ "get": {
+ "tags": [
+ "App"
+ ],
+ "summary": "Get app status",
+ "operationId": "getAppStatus",
+ "responses": {
+ "200": {
+ "description": "App is running",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiSuccess"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "message": "TeachLink API is running",
+ "timestamp": "2026-05-27T18:00:00.000Z"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/auth/register": {
+ "post": {
+ "tags": [
+ "Auth"
+ ],
+ "summary": "Register a new user",
+ "operationId": "registerUser",
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/RegisterRequest"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "email": "learner@example.com",
+ "password": "Password123!",
+ "firstName": "Ada",
+ "lastName": "Lovelace",
+ "role": "student"
+ }
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "201": {
+ "description": "Registration successful",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiSuccess"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "success": true,
+ "message": "Registration successful",
+ "data": {
+ "id": "2f4d8b5f-91d2-43a1-bd1e-877b4f97d7b9",
+ "email": "learner@example.com",
+ "firstName": "Ada",
+ "lastName": "Lovelace",
+ "role": "student",
+ "status": "active"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid registration data",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiError"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "success": false,
+ "message": "Validation failed",
+ "errors": [
+ {
+ "field": "email",
+ "message": "Validation failed"
+ }
+ ]
+ }
+ }
+ }
+ }
+ }
+ },
+ "409": {
+ "description": "Email already exists",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiError"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "success": false,
+ "message": "Email already exists",
+ "errors": []
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/auth/login": {
+ "post": {
+ "tags": [
+ "Auth"
+ ],
+ "summary": "Log in with email and password",
+ "operationId": "loginUser",
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/LoginRequest"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "email": "learner@example.com",
+ "password": "Password123!"
+ }
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Login successful",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiSuccess"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "success": true,
+ "message": "Login successful",
+ "data": {
+ "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
+ "refreshToken": "refresh_01JZ0D4R8R2Y3R9H2W6E5R4T1P",
+ "user": {
+ "id": "2f4d8b5f-91d2-43a1-bd1e-877b4f97d7b9",
+ "email": "learner@example.com",
+ "firstName": "Ada",
+ "lastName": "Lovelace",
+ "role": "student",
+ "status": "active"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Invalid credentials",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiError"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "success": false,
+ "message": "Invalid credentials",
+ "errors": []
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/users": {
+ "get": {
+ "tags": [
+ "Users"
+ ],
+ "summary": "List users",
+ "operationId": "listUsers",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "page",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "default": 1
+ }
+ },
+ {
+ "name": "limit",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "default": 20,
+ "maximum": 100
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Users found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiSuccess"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "success": true,
+ "message": "Operation completed successfully",
+ "data": [
+ {
+ "id": "2f4d8b5f-91d2-43a1-bd1e-877b4f97d7b9",
+ "email": "learner@example.com",
+ "firstName": "Ada",
+ "lastName": "Lovelace",
+ "role": "student",
+ "status": "active"
+ }
+ ]
+ }
+ }
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Authentication required",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiError"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "success": false,
+ "message": "Authentication required",
+ "errors": []
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "post": {
+ "tags": [
+ "Users"
+ ],
+ "summary": "Create a user",
+ "operationId": "createUser",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/RegisterRequest"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "email": "teacher@example.com",
+ "password": "Password123!",
+ "firstName": "Grace",
+ "lastName": "Hopper",
+ "role": "teacher"
+ }
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "201": {
+ "description": "User created",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiSuccess"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "success": true,
+ "message": "User created",
+ "data": {
+ "id": "2f4d8b5f-91d2-43a1-bd1e-877b4f97d7b9",
+ "email": "learner@example.com",
+ "firstName": "Ada",
+ "lastName": "Lovelace",
+ "role": "teacher",
+ "status": "active"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid user data",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiError"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "success": false,
+ "message": "Validation failed",
+ "errors": []
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/courses": {
+ "get": {
+ "tags": [
+ "Courses"
+ ],
+ "summary": "List courses",
+ "operationId": "listCourses",
+ "parameters": [
+ {
+ "name": "page",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "default": 1
+ }
+ },
+ {
+ "name": "limit",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "default": 20,
+ "maximum": 100
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Courses found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiSuccess"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "success": true,
+ "message": "Operation completed successfully",
+ "data": [
+ {
+ "id": "8e4fd4f8-d8f3-46b5-8786-6f7167a654f4",
+ "title": "JavaScript Foundations",
+ "description": "Learn modern JavaScript from first principles.",
+ "category": "programming",
+ "level": "beginner",
+ "price": 3999,
+ "status": "published"
+ }
+ ]
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "post": {
+ "tags": [
+ "Courses"
+ ],
+ "summary": "Create a course",
+ "operationId": "createCourse",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/CourseRequest"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "title": "JavaScript Foundations",
+ "description": "Learn modern JavaScript from first principles.",
+ "category": "programming",
+ "level": "beginner",
+ "price": 3999
+ }
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "201": {
+ "description": "Course created",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiSuccess"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "success": true,
+ "message": "Course created",
+ "data": {
+ "id": "8e4fd4f8-d8f3-46b5-8786-6f7167a654f4",
+ "title": "JavaScript Foundations",
+ "description": "Learn modern JavaScript from first principles.",
+ "category": "programming",
+ "level": "beginner",
+ "price": 3999,
+ "status": "published"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid course data",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiError"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "success": false,
+ "message": "Validation failed",
+ "errors": [
+ {
+ "field": "title",
+ "message": "Validation failed"
+ }
+ ]
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/payments/create-intent": {
+ "post": {
+ "tags": [
+ "Payments"
+ ],
+ "summary": "Create a payment intent",
+ "operationId": "createPaymentIntent",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "X-Idempotency-Key",
+ "in": "header",
+ "required": false,
+ "schema": {
+ "type": "string"
+ },
+ "example": "payment-8e4fd4f8-d8f3-46b5"
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/PaymentIntentRequest"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "courseId": "8e4fd4f8-d8f3-46b5-8786-6f7167a654f4",
+ "amount": 3999,
+ "currency": "USD"
+ }
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "201": {
+ "description": "Payment intent created",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiSuccess"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "success": true,
+ "message": "Payment intent created",
+ "data": {
+ "id": "pay_01JZ0D4R8R2Y3R9H2W6E5R4T1P",
+ "amount": 3999,
+ "currency": "USD",
+ "status": "pending",
+ "providerClientSecret": "pi_123_secret_456"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "409": {
+ "description": "Duplicate idempotency key",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiError"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "success": false,
+ "message": "Request already processed",
+ "errors": []
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/search": {
+ "get": {
+ "tags": [
+ "Search"
+ ],
+ "summary": "Search courses and learning content",
+ "operationId": "searchContent",
+ "parameters": [
+ {
+ "name": "q",
+ "in": "query",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "example": "javascript basics"
+ },
+ {
+ "name": "filters",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "string"
+ },
+ "example": "{\"category\":\"programming\",\"level\":\"beginner\"}"
+ },
+ {
+ "name": "sort",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "string"
+ },
+ "example": "relevance"
+ },
+ {
+ "name": "page",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "default": 1
+ }
+ },
+ {
+ "name": "limit",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "default": 20
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Search results",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/SearchResponse"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "results": [
+ {
+ "id": "8e4fd4f8-d8f3-46b5-8786-6f7167a654f4",
+ "title": "JavaScript Foundations",
+ "description": "Learn modern JavaScript from first principles.",
+ "category": "programming",
+ "level": "beginner",
+ "price": 3999,
+ "status": "published"
+ }
+ ],
+ "total": 1,
+ "page": 1,
+ "limit": 20,
+ "filters": {
+ "category": "programming",
+ "level": "beginner"
+ },
+ "query": "javascript basics"
+ }
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid filters JSON",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiError"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "success": false,
+ "message": "filters must be valid JSON",
+ "errors": [
+ {
+ "field": "filters",
+ "message": "filters must be valid JSON"
+ }
+ ]
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/search/autocomplete": {
+ "get": {
+ "tags": [
+ "Search"
+ ],
+ "summary": "Get search autocomplete suggestions",
+ "operationId": "getAutocomplete",
+ "parameters": [
+ {
+ "name": "q",
+ "in": "query",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "example": "java"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Autocomplete suggestions",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiSuccess"
+ },
+ "examples": {
+ "default": {
+ "value": [
+ "javascript",
+ "java fundamentals",
+ "java spring"
+ ]
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/debug/requests": {
+ "get": {
+ "tags": [
+ "Debugging"
+ ],
+ "summary": "List recently captured requests",
+ "operationId": "listCapturedRequests",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "limit",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "default": 50
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Captured request summaries",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiSuccess"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "total": 1,
+ "records": [
+ {
+ "id": "req_01",
+ "timestamp": "2026-05-27T18:00:00.000Z",
+ "method": "GET",
+ "path": "/search?q=javascript",
+ "statusCode": 200,
+ "durationMs": 18,
+ "hasError": false
+ }
+ ]
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "delete": {
+ "tags": [
+ "Debugging"
+ ],
+ "summary": "Clear the captured request buffer",
+ "operationId": "clearCapturedRequests",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Capture buffer cleared",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiSuccess"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "message": "Debug capture buffer cleared"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "components": {
+ "securitySchemes": {
+ "bearerAuth": {
+ "type": "http",
+ "scheme": "bearer",
+ "bearerFormat": "JWT"
+ }
+ },
+ "schemas": {
+ "ApiSuccess": {
+ "type": "object",
+ "properties": {
+ "success": {
+ "type": "boolean",
+ "example": true
+ },
+ "message": {
+ "type": "string",
+ "example": "Operation completed successfully"
+ },
+ "data": {
+ "type": "object"
+ }
+ }
+ },
+ "ApiError": {
+ "type": "object",
+ "properties": {
+ "success": {
+ "type": "boolean",
+ "example": false
+ },
+ "message": {
+ "type": "string",
+ "example": "Validation failed"
+ },
+ "errors": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "field": {
+ "type": "string",
+ "example": "email"
+ },
+ "message": {
+ "type": "string",
+ "example": "email must be valid"
+ }
+ }
+ }
+ }
+ }
+ },
+ "LoginRequest": {
+ "type": "object",
+ "required": [
+ "email",
+ "password"
+ ],
+ "properties": {
+ "email": {
+ "type": "string",
+ "format": "email",
+ "example": "learner@example.com"
+ },
+ "password": {
+ "type": "string",
+ "format": "password",
+ "example": "Password123!"
+ }
+ }
+ },
+ "RegisterRequest": {
+ "type": "object",
+ "required": [
+ "email",
+ "password",
+ "firstName",
+ "lastName"
+ ],
+ "properties": {
+ "email": {
+ "type": "string",
+ "format": "email",
+ "example": "learner@example.com"
+ },
+ "password": {
+ "type": "string",
+ "format": "password",
+ "example": "Password123!"
+ },
+ "firstName": {
+ "type": "string",
+ "example": "Ada"
+ },
+ "lastName": {
+ "type": "string",
+ "example": "Lovelace"
+ },
+ "role": {
+ "type": "string",
+ "enum": [
+ "student",
+ "teacher"
+ ],
+ "example": "student"
+ }
+ }
+ },
+ "CourseRequest": {
+ "type": "object",
+ "required": [
+ "title",
+ "description"
+ ],
+ "properties": {
+ "title": {
+ "type": "string",
+ "example": "JavaScript Foundations"
+ },
+ "description": {
+ "type": "string",
+ "example": "Learn modern JavaScript from first principles."
+ },
+ "category": {
+ "type": "string",
+ "example": "programming"
+ },
+ "level": {
+ "type": "string",
+ "example": "beginner"
+ },
+ "price": {
+ "type": "number",
+ "example": 3999
+ }
+ }
+ },
+ "PaymentIntentRequest": {
+ "type": "object",
+ "required": [
+ "courseId",
+ "amount",
+ "currency"
+ ],
+ "properties": {
+ "courseId": {
+ "type": "string",
+ "format": "uuid",
+ "example": "8e4fd4f8-d8f3-46b5-8786-6f7167a654f4"
+ },
+ "amount": {
+ "type": "number",
+ "example": 3999
+ },
+ "currency": {
+ "type": "string",
+ "example": "USD"
+ }
+ }
+ },
+ "SearchResponse": {
+ "type": "object",
+ "properties": {
+ "results": {
+ "type": "array",
+ "items": {
+ "type": "object"
+ }
+ },
+ "total": {
+ "type": "integer",
+ "example": 1
+ },
+ "page": {
+ "type": "integer",
+ "example": 1
+ },
+ "limit": {
+ "type": "integer",
+ "example": 20
+ },
+ "filters": {
+ "type": "object"
+ },
+ "query": {
+ "type": "string",
+ "example": "javascript basics"
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/docs/site/styles.css b/docs/site/styles.css
new file mode 100644
index 00000000..ced7c7c8
--- /dev/null
+++ b/docs/site/styles.css
@@ -0,0 +1,93 @@
+:root {
+ color-scheme: light;
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+ color: #172026;
+ background: #f8faf9;
+}
+
+body {
+ margin: 0;
+}
+
+main {
+ width: min(1180px, calc(100% - 32px));
+ margin: 0 auto;
+ padding: 40px 0 80px;
+}
+
+.hero {
+ padding: 48px 0 32px;
+ border-bottom: 1px solid #d9e2df;
+}
+
+.eyebrow {
+ color: #0d7a61;
+ font-weight: 700;
+ text-transform: uppercase;
+ font-size: 0.78rem;
+}
+
+h1 {
+ margin: 0;
+ font-size: 3rem;
+ letter-spacing: 0;
+}
+
+h2 {
+ margin-top: 40px;
+}
+
+.actions {
+ display: flex;
+ gap: 12px;
+ flex-wrap: wrap;
+ margin-top: 24px;
+}
+
+.actions a {
+ border: 1px solid #0d7a61;
+ color: #0b5f4d;
+ padding: 10px 14px;
+ border-radius: 6px;
+ text-decoration: none;
+ font-weight: 700;
+}
+
+table {
+ width: 100%;
+ border-collapse: collapse;
+ background: #ffffff;
+ border: 1px solid #d9e2df;
+}
+
+th, td {
+ padding: 12px;
+ border-bottom: 1px solid #e5ece9;
+ text-align: left;
+ vertical-align: top;
+}
+
+th {
+ background: #edf4f1;
+}
+
+code {
+ white-space: nowrap;
+}
+
+.method {
+ display: inline-block;
+ min-width: 56px;
+ padding: 4px 8px;
+ border-radius: 4px;
+ color: white;
+ font-weight: 800;
+ text-align: center;
+ font-size: 0.78rem;
+}
+
+.method-get { background: #2563eb; }
+.method-post { background: #0d7a61; }
+.method-put { background: #b45309; }
+.method-patch { background: #7c3aed; }
+.method-delete { background: #dc2626; }
diff --git a/openapi-spec.json b/openapi-spec.json
index 62f83a64..356525f5 100644
--- a/openapi-spec.json
+++ b/openapi-spec.json
@@ -1,217 +1,419 @@
{
- "openapi": "3.0.0",
+ "openapi": "3.0.3",
"info": {
"title": "TeachLink API",
- "description": "TeachLink Backend API Documentation",
- "version": "1.0"
+ "description": "Automatically generated OpenAPI documentation for TeachLink backend APIs, including request and response examples.",
+ "version": "0.0.1"
},
+ "servers": [
+ {
+ "url": "http://localhost:3000",
+ "description": "Local development"
+ },
+ {
+ "url": "https://api.teachlink.com",
+ "description": "Production"
+ }
+ ],
+ "tags": [
+ {
+ "name": "App",
+ "description": "Service metadata and status"
+ },
+ {
+ "name": "Auth",
+ "description": "Registration, login, and token management"
+ },
+ {
+ "name": "Users",
+ "description": "User account management"
+ },
+ {
+ "name": "Courses",
+ "description": "Course catalog and authoring"
+ },
+ {
+ "name": "Payments",
+ "description": "Payments, subscriptions, and refunds"
+ },
+ {
+ "name": "Search",
+ "description": "Search, filters, autocomplete, and analytics"
+ },
+ {
+ "name": "Debugging",
+ "description": "Admin-only request capture and replay tools"
+ }
+ ],
"paths": {
- "/users": {
+ "/": {
"get": {
"tags": [
- "users"
- ],
- "summary": "Get all users (Admin only)",
- "security": [
- {
- "bearerAuth": []
- }
+ "App"
],
+ "summary": "Get app status",
+ "operationId": "getAppStatus",
"responses": {
"200": {
- "description": "Users found"
+ "description": "App is running",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiSuccess"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "message": "TeachLink API is running",
+ "timestamp": "2026-05-27T18:00:00.000Z"
+ }
+ }
+ }
+ }
+ }
}
}
- },
+ }
+ },
+ "/auth/register": {
"post": {
"tags": [
- "users"
- ],
- "summary": "Create a new user (Admin only)",
- "security": [
- {
- "bearerAuth": []
- }
+ "Auth"
],
+ "summary": "Register a new user",
+ "operationId": "registerUser",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
- "type": "object",
- "properties": {
- "email": {
- "type": "string"
- },
- "password": {
- "type": "string"
- },
- "firstName": {
- "type": "string"
- },
- "lastName": {
- "type": "string"
+ "$ref": "#/components/schemas/RegisterRequest"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "email": "learner@example.com",
+ "password": "Password123!",
+ "firstName": "Ada",
+ "lastName": "Lovelace",
+ "role": "student"
}
- },
- "required": [
- "email",
- "password",
- "firstName",
- "lastName"
- ]
+ }
}
}
}
},
"responses": {
"201": {
- "description": "User created"
+ "description": "Registration successful",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiSuccess"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "success": true,
+ "message": "Registration successful",
+ "data": {
+ "id": "2f4d8b5f-91d2-43a1-bd1e-877b4f97d7b9",
+ "email": "learner@example.com",
+ "firstName": "Ada",
+ "lastName": "Lovelace",
+ "role": "student",
+ "status": "active"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid registration data",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiError"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "success": false,
+ "message": "Validation failed",
+ "errors": [
+ {
+ "field": "email",
+ "message": "Validation failed"
+ }
+ ]
+ }
+ }
+ }
+ }
+ }
+ },
+ "409": {
+ "description": "Email already exists",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiError"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "success": false,
+ "message": "Email already exists",
+ "errors": []
+ }
+ }
+ }
+ }
+ }
}
}
}
},
- "/users/{id}": {
- "get": {
+ "/auth/login": {
+ "post": {
"tags": [
- "users"
+ "Auth"
],
- "summary": "Get user by ID",
- "parameters": [
- {
- "name": "id",
- "in": "path",
- "required": true,
- "schema": {
- "type": "string"
+ "summary": "Log in with email and password",
+ "operationId": "loginUser",
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/LoginRequest"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "email": "learner@example.com",
+ "password": "Password123!"
+ }
+ }
+ }
}
}
- ],
+ },
"responses": {
"200": {
- "description": "User found"
+ "description": "Login successful",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiSuccess"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "success": true,
+ "message": "Login successful",
+ "data": {
+ "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
+ "refreshToken": "refresh_01JZ0D4R8R2Y3R9H2W6E5R4T1P",
+ "user": {
+ "id": "2f4d8b5f-91d2-43a1-bd1e-877b4f97d7b9",
+ "email": "learner@example.com",
+ "firstName": "Ada",
+ "lastName": "Lovelace",
+ "role": "student",
+ "status": "active"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Invalid credentials",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiError"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "success": false,
+ "message": "Invalid credentials",
+ "errors": []
+ }
+ }
+ }
+ }
+ }
}
}
- },
- "patch": {
+ }
+ },
+ "/users": {
+ "get": {
"tags": [
- "users"
+ "Users"
],
- "summary": "Update user",
- "parameters": [
+ "summary": "List users",
+ "operationId": "listUsers",
+ "security": [
{
- "name": "id",
- "in": "path",
- "required": true,
- "schema": {
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "description": "User updated"
+ "bearerAuth": []
}
- }
- },
- "delete": {
- "tags": [
- "users"
],
- "summary": "Delete user (Admin only)",
"parameters": [
{
- "name": "id",
- "in": "path",
- "required": true,
+ "name": "page",
+ "in": "query",
+ "required": false,
"schema": {
- "type": "string"
+ "type": "integer",
+ "default": 1
+ }
+ },
+ {
+ "name": "limit",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "default": 20,
+ "maximum": 100
}
}
],
"responses": {
"200": {
- "description": "User deleted"
- }
- }
- }
- },
- "/auth/login": {
- "post": {
- "tags": [
- "auth"
- ],
- "summary": "User login",
- "requestBody": {
- "required": true,
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "properties": {
- "email": {
- "type": "string"
- },
- "password": {
- "type": "string"
- }
+ "description": "Users found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiSuccess"
},
- "required": [
- "email",
- "password"
- ]
+ "examples": {
+ "default": {
+ "value": {
+ "success": true,
+ "message": "Operation completed successfully",
+ "data": [
+ {
+ "id": "2f4d8b5f-91d2-43a1-bd1e-877b4f97d7b9",
+ "email": "learner@example.com",
+ "firstName": "Ada",
+ "lastName": "Lovelace",
+ "role": "student",
+ "status": "active"
+ }
+ ]
+ }
+ }
+ }
}
}
- }
- },
- "responses": {
- "200": {
- "description": "Login successful"
},
"401": {
- "description": "Invalid credentials"
+ "description": "Authentication required",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiError"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "success": false,
+ "message": "Authentication required",
+ "errors": []
+ }
+ }
+ }
+ }
+ }
}
}
- }
- },
- "/auth/register": {
+ },
"post": {
"tags": [
- "auth"
+ "Users"
+ ],
+ "summary": "Create a user",
+ "operationId": "createUser",
+ "security": [
+ {
+ "bearerAuth": []
+ }
],
- "summary": "User registration",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
- "type": "object",
- "properties": {
- "email": {
- "type": "string"
- },
- "password": {
- "type": "string"
- },
- "firstName": {
- "type": "string"
- },
- "lastName": {
- "type": "string"
+ "$ref": "#/components/schemas/RegisterRequest"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "email": "teacher@example.com",
+ "password": "Password123!",
+ "firstName": "Grace",
+ "lastName": "Hopper",
+ "role": "teacher"
}
- },
- "required": [
- "email",
- "password",
- "firstName",
- "lastName"
- ]
+ }
}
}
}
},
"responses": {
"201": {
- "description": "Registration successful"
+ "description": "User created",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiSuccess"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "success": true,
+ "message": "User created",
+ "data": {
+ "id": "2f4d8b5f-91d2-43a1-bd1e-877b4f97d7b9",
+ "email": "learner@example.com",
+ "firstName": "Ada",
+ "lastName": "Lovelace",
+ "role": "teacher",
+ "status": "active"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid user data",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiError"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "success": false,
+ "message": "Validation failed",
+ "errors": []
+ }
+ }
+ }
+ }
+ }
}
}
}
@@ -219,20 +421,69 @@
"/courses": {
"get": {
"tags": [
- "courses"
+ "Courses"
+ ],
+ "summary": "List courses",
+ "operationId": "listCourses",
+ "parameters": [
+ {
+ "name": "page",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "default": 1
+ }
+ },
+ {
+ "name": "limit",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "default": 20,
+ "maximum": 100
+ }
+ }
],
- "summary": "Get all courses",
"responses": {
"200": {
- "description": "Courses found"
+ "description": "Courses found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiSuccess"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "success": true,
+ "message": "Operation completed successfully",
+ "data": [
+ {
+ "id": "8e4fd4f8-d8f3-46b5-8786-6f7167a654f4",
+ "title": "JavaScript Foundations",
+ "description": "Learn modern JavaScript from first principles.",
+ "category": "programming",
+ "level": "beginner",
+ "price": 3999,
+ "status": "published"
+ }
+ ]
+ }
+ }
+ }
+ }
+ }
}
}
},
"post": {
"tags": [
- "courses"
+ "Courses"
],
- "summary": "Create a new course",
+ "summary": "Create a course",
+ "operationId": "createCourse",
"security": [
{
"bearerAuth": []
@@ -243,116 +494,407 @@
"content": {
"application/json": {
"schema": {
- "type": "object",
- "properties": {
- "title": {
- "type": "string"
- },
- "description": {
- "type": "string"
+ "$ref": "#/components/schemas/CourseRequest"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "title": "JavaScript Foundations",
+ "description": "Learn modern JavaScript from first principles.",
+ "category": "programming",
+ "level": "beginner",
+ "price": 3999
}
- },
- "required": [
- "title"
- ]
+ }
}
}
}
},
"responses": {
"201": {
- "description": "Course created"
+ "description": "Course created",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiSuccess"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "success": true,
+ "message": "Course created",
+ "data": {
+ "id": "8e4fd4f8-d8f3-46b5-8786-6f7167a654f4",
+ "title": "JavaScript Foundations",
+ "description": "Learn modern JavaScript from first principles.",
+ "category": "programming",
+ "level": "beginner",
+ "price": 3999,
+ "status": "published"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid course data",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiError"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "success": false,
+ "message": "Validation failed",
+ "errors": [
+ {
+ "field": "title",
+ "message": "Validation failed"
+ }
+ ]
+ }
+ }
+ }
+ }
+ }
}
}
}
},
- "/courses/{id}": {
- "get": {
+ "/payments/create-intent": {
+ "post": {
"tags": [
- "courses"
+ "Payments"
+ ],
+ "summary": "Create a payment intent",
+ "operationId": "createPaymentIntent",
+ "security": [
+ {
+ "bearerAuth": []
+ }
],
- "summary": "Get course by ID",
"parameters": [
{
- "name": "id",
- "in": "path",
- "required": true,
+ "name": "X-Idempotency-Key",
+ "in": "header",
+ "required": false,
"schema": {
"type": "string"
- }
+ },
+ "example": "payment-8e4fd4f8-d8f3-46b5"
}
],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/PaymentIntentRequest"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "courseId": "8e4fd4f8-d8f3-46b5-8786-6f7167a654f4",
+ "amount": 3999,
+ "currency": "USD"
+ }
+ }
+ }
+ }
+ }
+ },
"responses": {
- "200": {
- "description": "Course found"
+ "201": {
+ "description": "Payment intent created",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiSuccess"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "success": true,
+ "message": "Payment intent created",
+ "data": {
+ "id": "pay_01JZ0D4R8R2Y3R9H2W6E5R4T1P",
+ "amount": 3999,
+ "currency": "USD",
+ "status": "pending",
+ "providerClientSecret": "pi_123_secret_456"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "409": {
+ "description": "Duplicate idempotency key",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiError"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "success": false,
+ "message": "Request already processed",
+ "errors": []
+ }
+ }
+ }
+ }
+ }
}
}
- },
- "patch": {
+ }
+ },
+ "/search": {
+ "get": {
"tags": [
- "courses"
+ "Search"
],
- "summary": "Update course",
+ "summary": "Search courses and learning content",
+ "operationId": "searchContent",
"parameters": [
{
- "name": "id",
- "in": "path",
+ "name": "q",
+ "in": "query",
"required": true,
"schema": {
"type": "string"
+ },
+ "example": "javascript basics"
+ },
+ {
+ "name": "filters",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "string"
+ },
+ "example": "{\"category\":\"programming\",\"level\":\"beginner\"}"
+ },
+ {
+ "name": "sort",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "string"
+ },
+ "example": "relevance"
+ },
+ {
+ "name": "page",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "default": 1
+ }
+ },
+ {
+ "name": "limit",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "default": 20
}
}
],
"responses": {
"200": {
- "description": "Course updated"
+ "description": "Search results",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/SearchResponse"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "results": [
+ {
+ "id": "8e4fd4f8-d8f3-46b5-8786-6f7167a654f4",
+ "title": "JavaScript Foundations",
+ "description": "Learn modern JavaScript from first principles.",
+ "category": "programming",
+ "level": "beginner",
+ "price": 3999,
+ "status": "published"
+ }
+ ],
+ "total": 1,
+ "page": 1,
+ "limit": 20,
+ "filters": {
+ "category": "programming",
+ "level": "beginner"
+ },
+ "query": "javascript basics"
+ }
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid filters JSON",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiError"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "success": false,
+ "message": "filters must be valid JSON",
+ "errors": [
+ {
+ "field": "filters",
+ "message": "filters must be valid JSON"
+ }
+ ]
+ }
+ }
+ }
+ }
+ }
}
}
- },
- "delete": {
+ }
+ },
+ "/search/autocomplete": {
+ "get": {
"tags": [
- "courses"
+ "Search"
],
- "summary": "Delete course",
+ "summary": "Get search autocomplete suggestions",
+ "operationId": "getAutocomplete",
"parameters": [
{
- "name": "id",
- "in": "path",
+ "name": "q",
+ "in": "query",
"required": true,
"schema": {
"type": "string"
- }
+ },
+ "example": "java"
}
],
"responses": {
"200": {
- "description": "Course deleted"
+ "description": "Autocomplete suggestions",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiSuccess"
+ },
+ "examples": {
+ "default": {
+ "value": [
+ "javascript",
+ "java fundamentals",
+ "java spring"
+ ]
+ }
+ }
+ }
+ }
}
}
}
},
- "/health": {
+ "/debug/requests": {
"get": {
"tags": [
- "health"
+ "Debugging"
+ ],
+ "summary": "List recently captured requests",
+ "operationId": "listCapturedRequests",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "limit",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "default": 50
+ }
+ }
],
- "summary": "Health check",
"responses": {
"200": {
- "description": "Service is healthy"
+ "description": "Captured request summaries",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiSuccess"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "total": 1,
+ "records": [
+ {
+ "id": "req_01",
+ "timestamp": "2026-05-27T18:00:00.000Z",
+ "method": "GET",
+ "path": "/search?q=javascript",
+ "statusCode": 200,
+ "durationMs": 18,
+ "hasError": false
+ }
+ ]
+ }
+ }
+ }
+ }
+ }
}
}
- }
- },
- "/health/liveness": {
- "get": {
+ },
+ "delete": {
"tags": [
- "health"
+ "Debugging"
+ ],
+ "summary": "Clear the captured request buffer",
+ "operationId": "clearCapturedRequests",
+ "security": [
+ {
+ "bearerAuth": []
+ }
],
- "summary": "Liveness probe",
"responses": {
"200": {
- "description": "Service is alive"
+ "description": "Capture buffer cleared",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiSuccess"
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "message": "Debug capture buffer cleared"
+ }
+ }
+ }
+ }
+ }
}
}
}
@@ -365,6 +907,191 @@
"scheme": "bearer",
"bearerFormat": "JWT"
}
+ },
+ "schemas": {
+ "ApiSuccess": {
+ "type": "object",
+ "properties": {
+ "success": {
+ "type": "boolean",
+ "example": true
+ },
+ "message": {
+ "type": "string",
+ "example": "Operation completed successfully"
+ },
+ "data": {
+ "type": "object"
+ }
+ }
+ },
+ "ApiError": {
+ "type": "object",
+ "properties": {
+ "success": {
+ "type": "boolean",
+ "example": false
+ },
+ "message": {
+ "type": "string",
+ "example": "Validation failed"
+ },
+ "errors": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "field": {
+ "type": "string",
+ "example": "email"
+ },
+ "message": {
+ "type": "string",
+ "example": "email must be valid"
+ }
+ }
+ }
+ }
+ }
+ },
+ "LoginRequest": {
+ "type": "object",
+ "required": [
+ "email",
+ "password"
+ ],
+ "properties": {
+ "email": {
+ "type": "string",
+ "format": "email",
+ "example": "learner@example.com"
+ },
+ "password": {
+ "type": "string",
+ "format": "password",
+ "example": "Password123!"
+ }
+ }
+ },
+ "RegisterRequest": {
+ "type": "object",
+ "required": [
+ "email",
+ "password",
+ "firstName",
+ "lastName"
+ ],
+ "properties": {
+ "email": {
+ "type": "string",
+ "format": "email",
+ "example": "learner@example.com"
+ },
+ "password": {
+ "type": "string",
+ "format": "password",
+ "example": "Password123!"
+ },
+ "firstName": {
+ "type": "string",
+ "example": "Ada"
+ },
+ "lastName": {
+ "type": "string",
+ "example": "Lovelace"
+ },
+ "role": {
+ "type": "string",
+ "enum": [
+ "student",
+ "teacher"
+ ],
+ "example": "student"
+ }
+ }
+ },
+ "CourseRequest": {
+ "type": "object",
+ "required": [
+ "title",
+ "description"
+ ],
+ "properties": {
+ "title": {
+ "type": "string",
+ "example": "JavaScript Foundations"
+ },
+ "description": {
+ "type": "string",
+ "example": "Learn modern JavaScript from first principles."
+ },
+ "category": {
+ "type": "string",
+ "example": "programming"
+ },
+ "level": {
+ "type": "string",
+ "example": "beginner"
+ },
+ "price": {
+ "type": "number",
+ "example": 3999
+ }
+ }
+ },
+ "PaymentIntentRequest": {
+ "type": "object",
+ "required": [
+ "courseId",
+ "amount",
+ "currency"
+ ],
+ "properties": {
+ "courseId": {
+ "type": "string",
+ "format": "uuid",
+ "example": "8e4fd4f8-d8f3-46b5-8786-6f7167a654f4"
+ },
+ "amount": {
+ "type": "number",
+ "example": 3999
+ },
+ "currency": {
+ "type": "string",
+ "example": "USD"
+ }
+ }
+ },
+ "SearchResponse": {
+ "type": "object",
+ "properties": {
+ "results": {
+ "type": "array",
+ "items": {
+ "type": "object"
+ }
+ },
+ "total": {
+ "type": "integer",
+ "example": 1
+ },
+ "page": {
+ "type": "integer",
+ "example": 1
+ },
+ "limit": {
+ "type": "integer",
+ "example": 20
+ },
+ "filters": {
+ "type": "object"
+ },
+ "query": {
+ "type": "string",
+ "example": "javascript basics"
+ }
+ }
+ }
}
}
-}
\ No newline at end of file
+}
diff --git a/package.json b/package.json
index 9048172c..3e2aa666 100644
--- a/package.json
+++ b/package.json
@@ -39,7 +39,9 @@
"migrate:rollback:count": "curl -s -X POST http://localhost:3000/migrations/rollback/${COUNT:-1} | npx json",
"migrate:rollback:to": "curl -s -X POST http://localhost:3000/migrations/rollback/to/${MIGRATION_NAME} | npx json",
"migrate:reset": "curl -s -X DELETE http://localhost:3000/migrations/reset | npx json",
- "sdk:generate:spec": "ts-node scripts/generate-openapi-spec.ts",
+ "docs:generate": "node scripts/generate-api-docs.js",
+ "docs:check": "npm run docs:generate && git diff --exit-code -- openapi-spec.json docs/api/openapi-spec.json docs/api/examples.md docs/site",
+ "sdk:generate:spec": "npm run docs:generate",
"sdk:generate:ts": "openapi-generator-cli generate -i openapi-spec.json -g typescript-axios -o sdk/typescript",
"sdk:generate:python": "openapi-generator-cli generate -i openapi-spec.json -g python -o sdk/python",
"sdk:generate": "npm run sdk:generate:spec && npm run sdk:generate:ts && npm run sdk:generate:python",
diff --git a/scripts/generate-api-docs.js b/scripts/generate-api-docs.js
new file mode 100644
index 00000000..60b4798d
--- /dev/null
+++ b/scripts/generate-api-docs.js
@@ -0,0 +1,650 @@
+const fs = require('fs');
+const path = require('path');
+
+const rootDir = path.resolve(__dirname, '..');
+const apiDocsDir = path.join(rootDir, 'docs', 'api');
+const siteDir = path.join(rootDir, 'docs', 'site');
+
+const json = (value) => JSON.stringify(value, null, 2);
+
+const successEnvelope = (data, message = 'Operation completed successfully') => ({
+ success: true,
+ message,
+ data,
+});
+
+const errorEnvelope = (message, field) => ({
+ success: false,
+ message,
+ errors: field ? [{ field, message }] : [],
+});
+
+const examples = {
+ user: {
+ id: '2f4d8b5f-91d2-43a1-bd1e-877b4f97d7b9',
+ email: 'learner@example.com',
+ firstName: 'Ada',
+ lastName: 'Lovelace',
+ role: 'student',
+ status: 'active',
+ },
+ course: {
+ id: '8e4fd4f8-d8f3-46b5-8786-6f7167a654f4',
+ title: 'JavaScript Foundations',
+ description: 'Learn modern JavaScript from first principles.',
+ category: 'programming',
+ level: 'beginner',
+ price: 3999,
+ status: 'published',
+ },
+ payment: {
+ id: 'pay_01JZ0D4R8R2Y3R9H2W6E5R4T1P',
+ amount: 3999,
+ currency: 'USD',
+ status: 'pending',
+ providerClientSecret: 'pi_123_secret_456',
+ },
+};
+
+const schemas = {
+ ApiSuccess: {
+ type: 'object',
+ properties: {
+ success: { type: 'boolean', example: true },
+ message: { type: 'string', example: 'Operation completed successfully' },
+ data: { type: 'object' },
+ },
+ },
+ ApiError: {
+ type: 'object',
+ properties: {
+ success: { type: 'boolean', example: false },
+ message: { type: 'string', example: 'Validation failed' },
+ errors: {
+ type: 'array',
+ items: {
+ type: 'object',
+ properties: {
+ field: { type: 'string', example: 'email' },
+ message: { type: 'string', example: 'email must be valid' },
+ },
+ },
+ },
+ },
+ },
+ LoginRequest: {
+ type: 'object',
+ required: ['email', 'password'],
+ properties: {
+ email: { type: 'string', format: 'email', example: 'learner@example.com' },
+ password: { type: 'string', format: 'password', example: 'Password123!' },
+ },
+ },
+ RegisterRequest: {
+ type: 'object',
+ required: ['email', 'password', 'firstName', 'lastName'],
+ properties: {
+ email: { type: 'string', format: 'email', example: 'learner@example.com' },
+ password: { type: 'string', format: 'password', example: 'Password123!' },
+ firstName: { type: 'string', example: 'Ada' },
+ lastName: { type: 'string', example: 'Lovelace' },
+ role: { type: 'string', enum: ['student', 'teacher'], example: 'student' },
+ },
+ },
+ CourseRequest: {
+ type: 'object',
+ required: ['title', 'description'],
+ properties: {
+ title: { type: 'string', example: examples.course.title },
+ description: { type: 'string', example: examples.course.description },
+ category: { type: 'string', example: examples.course.category },
+ level: { type: 'string', example: examples.course.level },
+ price: { type: 'number', example: examples.course.price },
+ },
+ },
+ PaymentIntentRequest: {
+ type: 'object',
+ required: ['courseId', 'amount', 'currency'],
+ properties: {
+ courseId: { type: 'string', format: 'uuid', example: examples.course.id },
+ amount: { type: 'number', example: 3999 },
+ currency: { type: 'string', example: 'USD' },
+ },
+ },
+ SearchResponse: {
+ type: 'object',
+ properties: {
+ results: { type: 'array', items: { type: 'object' } },
+ total: { type: 'integer', example: 1 },
+ page: { type: 'integer', example: 1 },
+ limit: { type: 'integer', example: 20 },
+ filters: { type: 'object' },
+ query: { type: 'string', example: 'javascript basics' },
+ },
+ },
+};
+
+const response = (status, description, example, schemaRef = '#/components/schemas/ApiSuccess') => ({
+ description,
+ content: {
+ 'application/json': {
+ schema: { $ref: schemaRef },
+ examples: {
+ default: { value: example },
+ },
+ },
+ },
+});
+
+const requestBody = (schemaRef, example) => ({
+ required: true,
+ content: {
+ 'application/json': {
+ schema: { $ref: schemaRef },
+ examples: {
+ default: { value: example },
+ },
+ },
+ },
+});
+
+const bearerSecurity = [{ bearerAuth: [] }];
+
+const spec = {
+ openapi: '3.0.3',
+ info: {
+ title: 'TeachLink API',
+ description:
+ 'Automatically generated OpenAPI documentation for TeachLink backend APIs, including request and response examples.',
+ version: process.env.npm_package_version || '0.0.1',
+ },
+ servers: [
+ { url: 'http://localhost:3000', description: 'Local development' },
+ { url: 'https://api.teachlink.com', description: 'Production' },
+ ],
+ tags: [
+ { name: 'App', description: 'Service metadata and status' },
+ { name: 'Auth', description: 'Registration, login, and token management' },
+ { name: 'Users', description: 'User account management' },
+ { name: 'Courses', description: 'Course catalog and authoring' },
+ { name: 'Payments', description: 'Payments, subscriptions, and refunds' },
+ { name: 'Search', description: 'Search, filters, autocomplete, and analytics' },
+ { name: 'Debugging', description: 'Admin-only request capture and replay tools' },
+ ],
+ paths: {
+ '/': {
+ get: {
+ tags: ['App'],
+ summary: 'Get app status',
+ operationId: 'getAppStatus',
+ responses: {
+ 200: response(200, 'App is running', {
+ message: 'TeachLink API is running',
+ timestamp: '2026-05-27T18:00:00.000Z',
+ }),
+ },
+ },
+ },
+ '/auth/register': {
+ post: {
+ tags: ['Auth'],
+ summary: 'Register a new user',
+ operationId: 'registerUser',
+ requestBody: requestBody('#/components/schemas/RegisterRequest', {
+ email: 'learner@example.com',
+ password: 'Password123!',
+ firstName: 'Ada',
+ lastName: 'Lovelace',
+ role: 'student',
+ }),
+ responses: {
+ 201: response(201, 'Registration successful', successEnvelope(examples.user, 'Registration successful')),
+ 400: response(400, 'Invalid registration data', errorEnvelope('Validation failed', 'email'), '#/components/schemas/ApiError'),
+ 409: response(409, 'Email already exists', errorEnvelope('Email already exists'), '#/components/schemas/ApiError'),
+ },
+ },
+ },
+ '/auth/login': {
+ post: {
+ tags: ['Auth'],
+ summary: 'Log in with email and password',
+ operationId: 'loginUser',
+ requestBody: requestBody('#/components/schemas/LoginRequest', {
+ email: 'learner@example.com',
+ password: 'Password123!',
+ }),
+ responses: {
+ 200: response(200, 'Login successful', successEnvelope({
+ accessToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
+ refreshToken: 'refresh_01JZ0D4R8R2Y3R9H2W6E5R4T1P',
+ user: examples.user,
+ }, 'Login successful')),
+ 401: response(401, 'Invalid credentials', errorEnvelope('Invalid credentials'), '#/components/schemas/ApiError'),
+ },
+ },
+ },
+ '/users': {
+ get: {
+ tags: ['Users'],
+ summary: 'List users',
+ operationId: 'listUsers',
+ security: bearerSecurity,
+ parameters: [
+ { name: 'page', in: 'query', required: false, schema: { type: 'integer', default: 1 } },
+ { name: 'limit', in: 'query', required: false, schema: { type: 'integer', default: 20, maximum: 100 } },
+ ],
+ responses: {
+ 200: response(200, 'Users found', successEnvelope([examples.user])),
+ 401: response(401, 'Authentication required', errorEnvelope('Authentication required'), '#/components/schemas/ApiError'),
+ },
+ },
+ post: {
+ tags: ['Users'],
+ summary: 'Create a user',
+ operationId: 'createUser',
+ security: bearerSecurity,
+ requestBody: requestBody('#/components/schemas/RegisterRequest', {
+ email: 'teacher@example.com',
+ password: 'Password123!',
+ firstName: 'Grace',
+ lastName: 'Hopper',
+ role: 'teacher',
+ }),
+ responses: {
+ 201: response(201, 'User created', successEnvelope({ ...examples.user, role: 'teacher' }, 'User created')),
+ 400: response(400, 'Invalid user data', errorEnvelope('Validation failed'), '#/components/schemas/ApiError'),
+ },
+ },
+ },
+ '/courses': {
+ get: {
+ tags: ['Courses'],
+ summary: 'List courses',
+ operationId: 'listCourses',
+ parameters: [
+ { name: 'page', in: 'query', required: false, schema: { type: 'integer', default: 1 } },
+ { name: 'limit', in: 'query', required: false, schema: { type: 'integer', default: 20, maximum: 100 } },
+ ],
+ responses: {
+ 200: response(200, 'Courses found', successEnvelope([examples.course])),
+ },
+ },
+ post: {
+ tags: ['Courses'],
+ summary: 'Create a course',
+ operationId: 'createCourse',
+ security: bearerSecurity,
+ requestBody: requestBody('#/components/schemas/CourseRequest', {
+ title: examples.course.title,
+ description: examples.course.description,
+ category: examples.course.category,
+ level: examples.course.level,
+ price: examples.course.price,
+ }),
+ responses: {
+ 201: response(201, 'Course created', successEnvelope(examples.course, 'Course created')),
+ 400: response(400, 'Invalid course data', errorEnvelope('Validation failed', 'title'), '#/components/schemas/ApiError'),
+ },
+ },
+ },
+ '/payments/create-intent': {
+ post: {
+ tags: ['Payments'],
+ summary: 'Create a payment intent',
+ operationId: 'createPaymentIntent',
+ security: bearerSecurity,
+ parameters: [
+ {
+ name: 'X-Idempotency-Key',
+ in: 'header',
+ required: false,
+ schema: { type: 'string' },
+ example: 'payment-8e4fd4f8-d8f3-46b5',
+ },
+ ],
+ requestBody: requestBody('#/components/schemas/PaymentIntentRequest', {
+ courseId: examples.course.id,
+ amount: 3999,
+ currency: 'USD',
+ }),
+ responses: {
+ 201: response(201, 'Payment intent created', successEnvelope(examples.payment, 'Payment intent created')),
+ 409: response(409, 'Duplicate idempotency key', errorEnvelope('Request already processed'), '#/components/schemas/ApiError'),
+ },
+ },
+ },
+ '/search': {
+ get: {
+ tags: ['Search'],
+ summary: 'Search courses and learning content',
+ operationId: 'searchContent',
+ parameters: [
+ { name: 'q', in: 'query', required: true, schema: { type: 'string' }, example: 'javascript basics' },
+ {
+ name: 'filters',
+ in: 'query',
+ required: false,
+ schema: { type: 'string' },
+ example: '{"category":"programming","level":"beginner"}',
+ },
+ { name: 'sort', in: 'query', required: false, schema: { type: 'string' }, example: 'relevance' },
+ { name: 'page', in: 'query', required: false, schema: { type: 'integer', default: 1 } },
+ { name: 'limit', in: 'query', required: false, schema: { type: 'integer', default: 20 } },
+ ],
+ responses: {
+ 200: response(200, 'Search results', {
+ results: [examples.course],
+ total: 1,
+ page: 1,
+ limit: 20,
+ filters: { category: 'programming', level: 'beginner' },
+ query: 'javascript basics',
+ }, '#/components/schemas/SearchResponse'),
+ 400: response(400, 'Invalid filters JSON', errorEnvelope('filters must be valid JSON', 'filters'), '#/components/schemas/ApiError'),
+ },
+ },
+ },
+ '/search/autocomplete': {
+ get: {
+ tags: ['Search'],
+ summary: 'Get search autocomplete suggestions',
+ operationId: 'getAutocomplete',
+ parameters: [{ name: 'q', in: 'query', required: true, schema: { type: 'string' }, example: 'java' }],
+ responses: {
+ 200: response(200, 'Autocomplete suggestions', ['javascript', 'java fundamentals', 'java spring']),
+ },
+ },
+ },
+ '/debug/requests': {
+ get: {
+ tags: ['Debugging'],
+ summary: 'List recently captured requests',
+ operationId: 'listCapturedRequests',
+ security: bearerSecurity,
+ parameters: [{ name: 'limit', in: 'query', required: false, schema: { type: 'integer', default: 50 } }],
+ responses: {
+ 200: response(200, 'Captured request summaries', {
+ total: 1,
+ records: [
+ {
+ id: 'req_01',
+ timestamp: '2026-05-27T18:00:00.000Z',
+ method: 'GET',
+ path: '/search?q=javascript',
+ statusCode: 200,
+ durationMs: 18,
+ hasError: false,
+ },
+ ],
+ }),
+ },
+ },
+ delete: {
+ tags: ['Debugging'],
+ summary: 'Clear the captured request buffer',
+ operationId: 'clearCapturedRequests',
+ security: bearerSecurity,
+ responses: {
+ 200: response(200, 'Capture buffer cleared', { message: 'Debug capture buffer cleared' }),
+ },
+ },
+ },
+ },
+ components: {
+ securitySchemes: {
+ bearerAuth: {
+ type: 'http',
+ scheme: 'bearer',
+ bearerFormat: 'JWT',
+ },
+ },
+ schemas,
+ },
+};
+
+function ensureDir(dir) {
+ fs.mkdirSync(dir, { recursive: true });
+}
+
+function writeFile(filePath, content) {
+ ensureDir(path.dirname(filePath));
+ fs.writeFileSync(filePath, content);
+}
+
+function generateExamplesMarkdown() {
+ const curls = [
+ {
+ title: 'Login',
+ command: [
+ 'curl -X POST http://localhost:3000/auth/login \\',
+ ' -H "Content-Type: application/json" \\',
+ ` -d '${JSON.stringify({ email: 'learner@example.com', password: 'Password123!' })}'`,
+ ].join('\n'),
+ response: successEnvelope({
+ accessToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
+ refreshToken: 'refresh_01JZ0D4R8R2Y3R9H2W6E5R4T1P',
+ user: examples.user,
+ }, 'Login successful'),
+ },
+ {
+ title: 'Create Course',
+ command: [
+ 'curl -X POST http://localhost:3000/courses \\',
+ ' -H "Authorization: Bearer YOUR_TOKEN" \\',
+ ' -H "Content-Type: application/json" \\',
+ ` -d '${JSON.stringify({
+ title: examples.course.title,
+ description: examples.course.description,
+ category: examples.course.category,
+ level: examples.course.level,
+ price: examples.course.price,
+ })}'`,
+ ].join('\n'),
+ response: successEnvelope(examples.course, 'Course created'),
+ },
+ {
+ title: 'Search Content',
+ command:
+ 'curl "http://localhost:3000/search?q=javascript%20basics&filters=%7B%22category%22%3A%22programming%22%7D"',
+ response: {
+ results: [examples.course],
+ total: 1,
+ page: 1,
+ limit: 20,
+ filters: { category: 'programming' },
+ query: 'javascript basics',
+ },
+ },
+ ];
+
+ return [
+ '# API Examples',
+ '',
+ 'This file is generated by `npm run docs:generate` from `scripts/generate-api-docs.js`.',
+ '',
+ ...curls.flatMap((item) => [
+ `## ${item.title}`,
+ '',
+ '```bash',
+ item.command,
+ '```',
+ '',
+ '```json',
+ json(item.response),
+ '```',
+ '',
+ ]),
+ ].join('\n');
+}
+
+function generateSiteHtml() {
+ const operations = Object.entries(spec.paths).flatMap(([route, methods]) =>
+ Object.entries(methods).map(([method, operation]) => ({ route, method, operation })),
+ );
+
+ const rows = operations
+ .map(
+ ({ route, method, operation }) => `
+
+ | ${method.toUpperCase()} |
+ ${route} |
+ ${operation.summary} |
+ ${operation.tags.join(', ')} |
+
`,
+ )
+ .join('');
+
+ return `
+
+
+
+
+ TeachLink API Documentation
+
+
+
+
+
+ Generated API Docs
+ TeachLink API
+ OpenAPI ${spec.openapi} documentation generated from the backend documentation source.
+
+
+
+ Endpoints
+
+
+ | Method | Path | Summary | Tags |
+
+ ${rows}
+
+
+
+ Interactive Reference
+
+
+
+
+
+`;
+}
+
+function generateStyles() {
+ return `:root {
+ color-scheme: light;
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+ color: #172026;
+ background: #f8faf9;
+}
+
+body {
+ margin: 0;
+}
+
+main {
+ width: min(1180px, calc(100% - 32px));
+ margin: 0 auto;
+ padding: 40px 0 80px;
+}
+
+.hero {
+ padding: 48px 0 32px;
+ border-bottom: 1px solid #d9e2df;
+}
+
+.eyebrow {
+ color: #0d7a61;
+ font-weight: 700;
+ text-transform: uppercase;
+ font-size: 0.78rem;
+}
+
+h1 {
+ margin: 0;
+ font-size: 3rem;
+ letter-spacing: 0;
+}
+
+h2 {
+ margin-top: 40px;
+}
+
+.actions {
+ display: flex;
+ gap: 12px;
+ flex-wrap: wrap;
+ margin-top: 24px;
+}
+
+.actions a {
+ border: 1px solid #0d7a61;
+ color: #0b5f4d;
+ padding: 10px 14px;
+ border-radius: 6px;
+ text-decoration: none;
+ font-weight: 700;
+}
+
+table {
+ width: 100%;
+ border-collapse: collapse;
+ background: #ffffff;
+ border: 1px solid #d9e2df;
+}
+
+th, td {
+ padding: 12px;
+ border-bottom: 1px solid #e5ece9;
+ text-align: left;
+ vertical-align: top;
+}
+
+th {
+ background: #edf4f1;
+}
+
+code {
+ white-space: nowrap;
+}
+
+.method {
+ display: inline-block;
+ min-width: 56px;
+ padding: 4px 8px;
+ border-radius: 4px;
+ color: white;
+ font-weight: 800;
+ text-align: center;
+ font-size: 0.78rem;
+}
+
+.method-get { background: #2563eb; }
+.method-post { background: #0d7a61; }
+.method-put { background: #b45309; }
+.method-patch { background: #7c3aed; }
+.method-delete { background: #dc2626; }
+`;
+}
+
+function main() {
+ ensureDir(apiDocsDir);
+ ensureDir(siteDir);
+
+ const specJson = json(spec);
+ writeFile(path.join(rootDir, 'openapi-spec.json'), `${specJson}\n`);
+ writeFile(path.join(apiDocsDir, 'openapi-spec.json'), `${specJson}\n`);
+ writeFile(path.join(apiDocsDir, 'examples.md'), generateExamplesMarkdown());
+ writeFile(path.join(siteDir, 'openapi-spec.json'), `${specJson}\n`);
+ writeFile(path.join(siteDir, 'examples.md'), generateExamplesMarkdown());
+ writeFile(path.join(siteDir, 'index.html'), generateSiteHtml());
+ writeFile(path.join(siteDir, 'styles.css'), generateStyles());
+ writeFile(path.join(siteDir, '.nojekyll'), '');
+
+ console.log(`Generated OpenAPI spec with ${Object.keys(spec.paths).length} paths.`);
+ console.log(`Wrote ${path.relative(rootDir, path.join(siteDir, 'index.html'))}`);
+}
+
+main();
diff --git a/scripts/generate-openapi-spec.ts b/scripts/generate-openapi-spec.ts
index d7d67b2a..f171e5b5 100644
--- a/scripts/generate-openapi-spec.ts
+++ b/scripts/generate-openapi-spec.ts
@@ -1,256 +1,7 @@
-import * as fs from 'fs';
-import * as path from 'path';
-
-const spec = {
- openapi: '3.0.0',
- info: {
- title: 'TeachLink API',
- description: 'TeachLink Backend API Documentation',
- version: '1.0',
- },
- paths: {
- '/users': {
- get: {
- tags: ['users'],
- summary: 'Get all users (Admin only)',
- security: [{ bearerAuth: [] }],
- responses: {
- 200: { description: 'Users found' },
- },
- },
- post: {
- tags: ['users'],
- summary: 'Create a new user (Admin only)',
- security: [{ bearerAuth: [] }],
- requestBody: {
- required: true,
- content: {
- 'application/json': {
- schema: {
- type: 'object',
- properties: {
- email: { type: 'string' },
- password: { type: 'string' },
- firstName: { type: 'string' },
- lastName: { type: 'string' },
- },
- required: ['email', 'password', 'firstName', 'lastName'],
- },
- },
- },
- },
- responses: {
- 201: { description: 'User created' },
- },
- },
- },
- '/users/{id}': {
- get: {
- tags: ['users'],
- summary: 'Get user by ID',
- parameters: [
- {
- name: 'id',
- in: 'path',
- required: true,
- schema: { type: 'string' },
- },
- ],
- responses: {
- 200: { description: 'User found' },
- },
- },
- patch: {
- tags: ['users'],
- summary: 'Update user',
- parameters: [
- {
- name: 'id',
- in: 'path',
- required: true,
- schema: { type: 'string' },
- },
- ],
- responses: {
- 200: { description: 'User updated' },
- },
- },
- delete: {
- tags: ['users'],
- summary: 'Delete user (Admin only)',
- parameters: [
- {
- name: 'id',
- in: 'path',
- required: true,
- schema: { type: 'string' },
- },
- ],
- responses: {
- 200: { description: 'User deleted' },
- },
- },
- },
- '/auth/login': {
- post: {
- tags: ['auth'],
- summary: 'User login',
- requestBody: {
- required: true,
- content: {
- 'application/json': {
- schema: {
- type: 'object',
- properties: {
- email: { type: 'string' },
- password: { type: 'string' },
- },
- required: ['email', 'password'],
- },
- },
- },
- },
- responses: {
- 200: { description: 'Login successful' },
- 401: { description: 'Invalid credentials' },
- },
- },
- },
- '/auth/register': {
- post: {
- tags: ['auth'],
- summary: 'User registration',
- requestBody: {
- required: true,
- content: {
- 'application/json': {
- schema: {
- type: 'object',
- properties: {
- email: { type: 'string' },
- password: { type: 'string' },
- firstName: { type: 'string' },
- lastName: { type: 'string' },
- },
- required: ['email', 'password', 'firstName', 'lastName'],
- },
- },
- },
- },
- responses: {
- 201: { description: 'Registration successful' },
- },
- },
- },
- '/courses': {
- get: {
- tags: ['courses'],
- summary: 'Get all courses',
- responses: {
- 200: { description: 'Courses found' },
- },
- },
- post: {
- tags: ['courses'],
- summary: 'Create a new course',
- security: [{ bearerAuth: [] }],
- requestBody: {
- required: true,
- content: {
- 'application/json': {
- schema: {
- type: 'object',
- properties: {
- title: { type: 'string' },
- description: { type: 'string' },
- },
- required: ['title'],
- },
- },
- },
- },
- responses: {
- 201: { description: 'Course created' },
- },
- },
- },
- '/courses/{id}': {
- get: {
- tags: ['courses'],
- summary: 'Get course by ID',
- parameters: [
- {
- name: 'id',
- in: 'path',
- required: true,
- schema: { type: 'string' },
- },
- ],
- responses: {
- 200: { description: 'Course found' },
- },
- },
- patch: {
- tags: ['courses'],
- summary: 'Update course',
- parameters: [
- {
- name: 'id',
- in: 'path',
- required: true,
- schema: { type: 'string' },
- },
- ],
- responses: {
- 200: { description: 'Course updated' },
- },
- },
- delete: {
- tags: ['courses'],
- summary: 'Delete course',
- parameters: [
- {
- name: 'id',
- in: 'path',
- required: true,
- schema: { type: 'string' },
- },
- ],
- responses: {
- 200: { description: 'Course deleted' },
- },
- },
- },
- '/health': {
- get: {
- tags: ['health'],
- summary: 'Health check',
- responses: {
- 200: { description: 'Service is healthy' },
- },
- },
- },
- '/health/liveness': {
- get: {
- tags: ['health'],
- summary: 'Liveness probe',
- responses: {
- 200: { description: 'Service is alive' },
- },
- },
- },
- },
- components: {
- securitySchemes: {
- bearerAuth: {
- type: 'http',
- scheme: 'bearer',
- bearerFormat: 'JWT',
- },
- },
- },
-};
-
-const outputPath = path.join(__dirname, '../openapi-spec.json');
-fs.writeFileSync(outputPath, JSON.stringify(spec, null, 2));
-console.log('OpenAPI spec generated:', outputPath);
\ No newline at end of file
+/**
+ * Backward-compatible entry point for older SDK workflows.
+ *
+ * The documentation generator now writes the OpenAPI spec, static docs site,
+ * and example request/response artifacts in one pass.
+ */
+require('./generate-api-docs.js');
diff --git a/src/ab-testing/ab-testing.controller.ts b/src/ab-testing/ab-testing.controller.ts
index 4682ea78..fb55e87f 100644
--- a/src/ab-testing/ab-testing.controller.ts
+++ b/src/ab-testing/ab-testing.controller.ts
@@ -12,6 +12,7 @@ import {
HttpStatus,
UseGuards,
} from '@nestjs/common';
+import { ApiBearerAuth, ApiResponse, ApiTags } from '@nestjs/swagger';
import { ABTestingService, ICreateExperimentDto } from './ab-testing.service';
import { ExperimentService } from './experiments/experiment.service';
import { StatisticalAnalysisService } from './analysis/statistical-analysis.service';
@@ -25,8 +26,12 @@ import { UserRole } from '../users/entities/user.entity';
/**
* Exposes AB testing endpoints.
*/
+@ApiTags('A/B Testing')
@Controller('ab-testing')
@UseGuards(JwtAuthGuard, RolesGuard)
+@ApiBearerAuth()
+@ApiResponse({ status: 401, description: 'Authentication required' })
+@ApiResponse({ status: 403, description: 'Insufficient role for this experiment operation' })
export class ABTestingController {
private readonly logger = new Logger(ABTestingController.name);
diff --git a/src/analytics/analytics.controller.ts b/src/analytics/analytics.controller.ts
index b542b2e5..d56e071a 100644
--- a/src/analytics/analytics.controller.ts
+++ b/src/analytics/analytics.controller.ts
@@ -1,4 +1,6 @@
import { Controller } from '@nestjs/common';
+import { ApiTags } from '@nestjs/swagger';
+@ApiTags('Analytics')
@Controller('analytics')
export class AnalyticsController {}
diff --git a/src/app.controller.ts b/src/app.controller.ts
index d2136437..8ed9f082 100644
--- a/src/app.controller.ts
+++ b/src/app.controller.ts
@@ -8,7 +8,16 @@ import { SkipQuota } from './rate-limiting/decorators/quota.decorator';
export class AppController {
@Get()
@ApiOperation({ summary: 'Get app status' })
- @ApiResponse({ status: 200, description: 'App is running' })
+ @ApiResponse({
+ status: 200,
+ description: 'App is running',
+ schema: {
+ example: {
+ message: 'TeachLink API is running',
+ timestamp: '2026-05-27T18:00:00.000Z',
+ },
+ },
+ })
getStatus() {
return { message: 'TeachLink API is running', timestamp: new Date().toISOString() };
}
diff --git a/src/assessment/assessment.controller.ts b/src/assessment/assessment.controller.ts
index 5c4b4ee3..5a1ce9eb 100644
--- a/src/assessment/assessment.controller.ts
+++ b/src/assessment/assessment.controller.ts
@@ -1,10 +1,13 @@
import { Body, Controller, Get, Param, Post, UseGuards, Request } from '@nestjs/common';
+import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { AssessmentsService } from './assessments.service';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
/**
* Exposes assessments endpoints.
*/
+@ApiTags('Assessments')
+@ApiBearerAuth()
@Controller('assessments')
export class AssessmentsController {
constructor(private readonly service: AssessmentsService) {}
@@ -17,6 +20,9 @@ export class AssessmentsController {
*/
@Post(':id/start')
@UseGuards(JwtAuthGuard)
+ @ApiOperation({ summary: 'Start an assessment attempt' })
+ @ApiResponse({ status: 201, description: 'Assessment attempt started' })
+ @ApiResponse({ status: 401, description: 'Authentication required' })
start(@Request() req: any, @Param('id') id: string): any {
const studentId = req.user.id;
if (!studentId) {
@@ -34,6 +40,9 @@ export class AssessmentsController {
*/
@Post('attempts/:id/submit')
@UseGuards(JwtAuthGuard)
+ @ApiOperation({ summary: 'Submit assessment answers' })
+ @ApiResponse({ status: 201, description: 'Assessment submitted and scored' })
+ @ApiResponse({ status: 401, description: 'Authentication required' })
submit(@Request() req: any, @Param('id') id: string, @Body('answers') answers: any[]): any {
return this.service.submitAssessment(id, answers);
}
@@ -46,6 +55,9 @@ export class AssessmentsController {
*/
@Get('attempts/:id')
@UseGuards(JwtAuthGuard)
+ @ApiOperation({ summary: 'Get assessment attempt results' })
+ @ApiResponse({ status: 200, description: 'Assessment attempt results' })
+ @ApiResponse({ status: 401, description: 'Authentication required' })
results(@Request() req: any, @Param('id') id: string): any {
return this.service.getResults(id);
}
diff --git a/src/common/controllers/circuit-breaker.controller.ts b/src/common/controllers/circuit-breaker.controller.ts
index 87e40d9d..392d833b 100644
--- a/src/common/controllers/circuit-breaker.controller.ts
+++ b/src/common/controllers/circuit-breaker.controller.ts
@@ -1,5 +1,5 @@
import { Controller, Get, Post, Param, UseGuards } from '@nestjs/common';
-import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
+import { ApiTags, ApiOperation, ApiBearerAuth, ApiResponse } from '@nestjs/swagger';
import { EnhancedCircuitBreakerService } from '../services/circuit-breaker.service';
import { Roles } from '../../auth/decorators/roles.decorator';
import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard';
@@ -10,12 +10,15 @@ import { UserRole } from '../../users/entities/user.entity';
@Controller('circuit-breakers')
@UseGuards(JwtAuthGuard, RolesGuard)
@ApiBearerAuth()
+@ApiResponse({ status: 401, description: 'Authentication required' })
+@ApiResponse({ status: 403, description: 'Admin role required' })
export class CircuitBreakerController {
constructor(private readonly circuitBreakerService: EnhancedCircuitBreakerService) {}
@Get()
@Roles(UserRole.ADMIN)
@ApiOperation({ summary: 'Get all circuit breaker statistics' })
+ @ApiResponse({ status: 200, description: 'Circuit breaker statistics' })
getAllStats() {
return this.circuitBreakerService.getAllStats();
}
@@ -23,6 +26,7 @@ export class CircuitBreakerController {
@Get('health')
@Roles(UserRole.ADMIN)
@ApiOperation({ summary: 'Get circuit breaker health status' })
+ @ApiResponse({ status: 200, description: 'Circuit breaker health status' })
getHealthStatus() {
return this.circuitBreakerService.getHealthStatus();
}
@@ -30,6 +34,7 @@ export class CircuitBreakerController {
@Get(':key')
@Roles(UserRole.ADMIN)
@ApiOperation({ summary: 'Get specific circuit breaker statistics' })
+ @ApiResponse({ status: 200, description: 'Circuit breaker statistics or not-found payload' })
getStats(@Param('key') key: string) {
const stats = this.circuitBreakerService.getStats(key);
if (!stats) {
@@ -41,6 +46,7 @@ export class CircuitBreakerController {
@Post(':key/reset')
@Roles(UserRole.ADMIN)
@ApiOperation({ summary: 'Reset a circuit breaker' })
+ @ApiResponse({ status: 201, description: 'Circuit breaker reset' })
resetCircuitBreaker(@Param('key') key: string) {
this.circuitBreakerService.close(key);
return { message: `Circuit breaker ${key} has been reset` };
@@ -49,6 +55,7 @@ export class CircuitBreakerController {
@Post(':key/disable')
@Roles(UserRole.ADMIN)
@ApiOperation({ summary: 'Disable a circuit breaker' })
+ @ApiResponse({ status: 201, description: 'Circuit breaker disabled' })
disableCircuitBreaker(@Param('key') key: string) {
this.circuitBreakerService.disable(key);
return { message: `Circuit breaker ${key} has been disabled` };
@@ -57,6 +64,7 @@ export class CircuitBreakerController {
@Post(':key/enable')
@Roles(UserRole.ADMIN)
@ApiOperation({ summary: 'Enable a circuit breaker' })
+ @ApiResponse({ status: 201, description: 'Circuit breaker enabled' })
enableCircuitBreaker(@Param('key') key: string) {
this.circuitBreakerService.enable(key);
return { message: `Circuit breaker ${key} has been enabled` };
diff --git a/src/common/interceptors/api-version.interceptor.ts b/src/common/interceptors/api-version.interceptor.ts
new file mode 100644
index 00000000..d74fcb0c
--- /dev/null
+++ b/src/common/interceptors/api-version.interceptor.ts
@@ -0,0 +1,31 @@
+import {
+ BadRequestException,
+ CallHandler,
+ ExecutionContext,
+ Injectable,
+ NestInterceptor,
+} from '@nestjs/common';
+import { Observable } from 'rxjs';
+
+export const API_VERSION_HEADER = 'X-API-Version';
+export const DEFAULT_API_VERSION = process.env.API_DEFAULT_VERSION || '1';
+export const SUPPORTED_API_VERSIONS = (process.env.API_SUPPORTED_VERSIONS || '1')
+ .split(',')
+ .map((version) => version.trim())
+ .filter(Boolean);
+
+@Injectable()
+export class ApiVersionInterceptor implements NestInterceptor {
+ intercept(context: ExecutionContext, next: CallHandler): Observable {
+ const request = context.switchToHttp().getRequest();
+ const version = request.headers?.[API_VERSION_HEADER.toLowerCase()] || DEFAULT_API_VERSION;
+
+ if (!SUPPORTED_API_VERSIONS.includes(String(version))) {
+ throw new BadRequestException(
+ `Unsupported API version "${version}". Supported versions: ${SUPPORTED_API_VERSIONS.join(', ')}`,
+ );
+ }
+
+ return next.handle();
+ }
+}
diff --git a/src/common/interceptors/global-exception.filter.ts b/src/common/interceptors/global-exception.filter.ts
new file mode 100644
index 00000000..6e1334b8
--- /dev/null
+++ b/src/common/interceptors/global-exception.filter.ts
@@ -0,0 +1,32 @@
+import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus } from '@nestjs/common';
+import { Request, Response } from 'express';
+import { getCorrelationId } from '../utils/correlation.utils';
+
+@Catch()
+export class GlobalExceptionFilter implements ExceptionFilter {
+ catch(exception: unknown, host: ArgumentsHost): void {
+ const ctx = host.switchToHttp();
+ const response = ctx.getResponse();
+ const request = ctx.getRequest();
+ const isHttpException = exception instanceof HttpException;
+ const status = isHttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR;
+ const exceptionResponse = isHttpException ? exception.getResponse() : undefined;
+ const message =
+ typeof exceptionResponse === 'object' &&
+ exceptionResponse !== null &&
+ 'message' in exceptionResponse
+ ? (exceptionResponse as { message: string | string[] }).message
+ : exception instanceof Error
+ ? exception.message
+ : 'Internal server error';
+
+ response.status(status).json({
+ success: false,
+ statusCode: status,
+ message,
+ path: request.url,
+ timestamp: new Date().toISOString(),
+ correlationId: getCorrelationId(),
+ });
+ }
+}
diff --git a/src/common/interceptors/response-transform.interceptor.ts b/src/common/interceptors/response-transform.interceptor.ts
new file mode 100644
index 00000000..3867e16d
--- /dev/null
+++ b/src/common/interceptors/response-transform.interceptor.ts
@@ -0,0 +1,22 @@
+import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
+import { Observable, map } from 'rxjs';
+import { getCorrelationId } from '../utils/correlation.utils';
+
+@Injectable()
+export class ResponseTransformInterceptor implements NestInterceptor {
+ intercept(context: ExecutionContext, next: CallHandler): Observable {
+ return next.handle().pipe(
+ map((data) => {
+ if (data && typeof data === 'object' && 'success' in data) {
+ return data;
+ }
+
+ return {
+ success: true,
+ data,
+ correlationId: getCorrelationId(),
+ };
+ }),
+ );
+ }
+}
diff --git a/src/common/middleware/decompression.middleware.spec.ts b/src/common/middleware/decompression.middleware.spec.ts
index a7e48572..15c91ab8 100644
--- a/src/common/middleware/decompression.middleware.spec.ts
+++ b/src/common/middleware/decompression.middleware.spec.ts
@@ -69,7 +69,7 @@ describe('DecompressionMiddleware', () => {
});
it('should pass through when content-encoding is not a string', () => {
- req.headers = { 'content-encoding': ['gzip', 'deflate'] };
+ req.headers = { 'content-encoding': ['gzip', 'deflate'] as any };
middleware.use(req as Request, res as Response, next);
expect(next).toHaveBeenCalled();
@@ -150,6 +150,8 @@ describe('DecompressionMiddleware', () => {
it('should handle decompression errors', (done) => {
req.headers = { 'content-encoding': 'gzip' };
const mockDecompressor = new PassThrough();
+ (mockDecompressor.pipe as any) = jest.fn().mockReturnValue(mockDecompressor);
+ (middleware as any).decompressors.gzip = () => mockDecompressor;
req.pipe = jest.fn().mockReturnValue(mockDecompressor);
req.on = jest.fn();
diff --git a/src/common/modules/api-versioning.module.ts b/src/common/modules/api-versioning.module.ts
new file mode 100644
index 00000000..d6693d5c
--- /dev/null
+++ b/src/common/modules/api-versioning.module.ts
@@ -0,0 +1,7 @@
+import { Module } from '@nestjs/common';
+
+export const API_VERSIONING_DOCUMENTATION =
+ 'Send X-API-Version with versioned requests. The default supported API version is 1.';
+
+@Module({})
+export class ApiVersioningModule {}
diff --git a/src/debugging/debug.controller.ts b/src/debugging/debug.controller.ts
index 0d778d6d..fd419dce 100644
--- a/src/debugging/debug.controller.ts
+++ b/src/debugging/debug.controller.ts
@@ -9,7 +9,7 @@ import {
Query,
UseGuards,
} from '@nestjs/common';
-import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
+import { ApiTags, ApiOperation, ApiBearerAuth, ApiResponse } from '@nestjs/swagger';
import { Roles } from '../auth/decorators/roles.decorator';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { RolesGuard } from '../auth/guards/roles.guard';
@@ -37,11 +37,29 @@ export class DebugController {
@Get('requests')
@Roles(UserRole.ADMIN)
@ApiOperation({ summary: 'List recently captured request/response exchanges' })
+ @ApiResponse({
+ status: 200,
+ description: 'Captured request summaries',
+ schema: {
+ example: {
+ total: 1,
+ records: [
+ {
+ id: 'req_01',
+ timestamp: '2026-05-27T18:00:00.000Z',
+ method: 'GET',
+ path: '/search?q=javascript',
+ statusCode: 200,
+ durationMs: 18,
+ hasError: false,
+ },
+ ],
+ },
+ },
+ })
list(@Query('limit') limit?: string) {
const parsed = limit ? Number(limit) : undefined;
- const records = this.capture.list(
- Number.isFinite(parsed) ? parsed : undefined,
- );
+ const records = this.capture.list(Number.isFinite(parsed) ? parsed : undefined);
// Summaries only — keep the list view light. Full payload via :id.
return {
total: this.capture.size,
@@ -60,6 +78,8 @@ export class DebugController {
@Get('requests/:id')
@Roles(UserRole.ADMIN)
@ApiOperation({ summary: 'Inspect a captured request/response in full' })
+ @ApiResponse({ status: 200, description: 'Captured request details' })
+ @ApiResponse({ status: 404, description: 'Captured request not found' })
inspect(@Param('id') id: string) {
const record = this.capture.get(id);
if (!record) throw new NotFoundException(`No captured request "${id}"`);
@@ -69,6 +89,8 @@ export class DebugController {
@Get('requests/:id/timeline')
@Roles(UserRole.ADMIN)
@ApiOperation({ summary: 'Get the performance timeline for a captured request' })
+ @ApiResponse({ status: 200, description: 'Captured request performance timeline' })
+ @ApiResponse({ status: 404, description: 'Captured request not found' })
timeline(@Param('id') id: string) {
const record = this.capture.get(id);
if (!record) throw new NotFoundException(`No captured request "${id}"`);
@@ -81,6 +103,8 @@ export class DebugController {
@Get('requests/:id/trace')
@Roles(UserRole.ADMIN)
@ApiOperation({ summary: 'Get the enhanced stack trace for a failed request' })
+ @ApiResponse({ status: 200, description: 'Enhanced error trace or no-error message' })
+ @ApiResponse({ status: 404, description: 'Captured request not found' })
trace(@Param('id') id: string) {
const record = this.capture.get(id);
if (!record) throw new NotFoundException(`No captured request "${id}"`);
@@ -93,6 +117,7 @@ export class DebugController {
@Post('requests/:id/replay')
@Roles(UserRole.ADMIN)
@ApiOperation({ summary: 'Replay a captured request and diff the response' })
+ @ApiResponse({ status: 200, description: 'Replay result and response diff' })
replay(@Param('id') id: string, @Body() body: ReplayRequestDto) {
return this.replayService.replay(id, {
baseUrl: body?.baseUrl,
@@ -104,6 +129,11 @@ export class DebugController {
@Delete('requests')
@Roles(UserRole.ADMIN)
@ApiOperation({ summary: 'Clear the captured request buffer' })
+ @ApiResponse({
+ status: 200,
+ description: 'Capture buffer cleared',
+ schema: { example: { message: 'Debug capture buffer cleared' } },
+ })
clear() {
this.capture.clear();
return { message: 'Debug capture buffer cleared' };
diff --git a/src/debugging/services/request-capture.service.ts b/src/debugging/services/request-capture.service.ts
index d309f060..9d385057 100644
--- a/src/debugging/services/request-capture.service.ts
+++ b/src/debugging/services/request-capture.service.ts
@@ -1,4 +1,4 @@
-import { Injectable } from '@nestjs/common';
+import { Inject, Injectable, Optional } from '@nestjs/common';
import { IDebugRecord } from '../interfaces/debug.interfaces';
/**
@@ -20,6 +20,8 @@ const DEFAULT_CONFIG: CaptureConfig = {
redactedHeaders: ['authorization', 'cookie', 'set-cookie', 'x-api-key'],
};
+export const DEBUG_CAPTURE_CONFIG = 'DEBUG_CAPTURE_CONFIG';
+
/**
* Stores captured request/response exchanges in a bounded in-memory ring
* buffer. This is the backing store for the inspection and replay endpoints.
@@ -32,7 +34,7 @@ export class RequestCaptureService {
private readonly config: CaptureConfig;
private readonly buffer: IDebugRecord[] = [];
- constructor(config?: Partial) {
+ constructor(@Optional() @Inject(DEBUG_CAPTURE_CONFIG) config?: Partial) {
this.config = { ...DEFAULT_CONFIG, ...config };
}
@@ -69,9 +71,7 @@ export class RequestCaptureService {
const redactHeaders = (headers: Record) => {
const out: Record = {};
for (const [key, value] of Object.entries(headers)) {
- out[key] = this.config.redactedHeaders.includes(key.toLowerCase())
- ? '[REDACTED]'
- : value;
+ out[key] = this.config.redactedHeaders.includes(key.toLowerCase()) ? '[REDACTED]' : value;
}
return out;
};
diff --git a/src/email-marketing/automation/automation.controller.ts b/src/email-marketing/automation/automation.controller.ts
index 342953cf..21c2b4b0 100644
--- a/src/email-marketing/automation/automation.controller.ts
+++ b/src/email-marketing/automation/automation.controller.ts
@@ -22,6 +22,7 @@ import { AutomationWorkflow } from '../entities/automation-workflow.entity';
*/
@ApiTags('Email Marketing - Automation')
@ApiBearerAuth()
+@ApiResponse({ status: 401, description: 'Authentication required' })
@Controller('email-marketing/automation')
export class AutomationController {
constructor(private readonly automationService: AutomationService) {}
diff --git a/src/email-marketing/templates/template.controller.ts b/src/email-marketing/templates/template.controller.ts
index 69d82bec..a70d5630 100644
--- a/src/email-marketing/templates/template.controller.ts
+++ b/src/email-marketing/templates/template.controller.ts
@@ -22,6 +22,7 @@ import { EmailTemplate } from '../entities/email-template.entity';
*/
@ApiTags('Email Marketing - Templates')
@ApiBearerAuth()
+@ApiResponse({ status: 401, description: 'Authentication required' })
@Controller('email-marketing/templates')
export class TemplateController {
constructor(private readonly templateService: TemplateManagementService) {}
@@ -48,6 +49,7 @@ export class TemplateController {
@ApiOperation({ summary: 'Get all email templates' })
@ApiQuery({ name: 'page', required: false, type: Number })
@ApiQuery({ name: 'limit', required: false, type: Number })
+ @ApiResponse({ status: 200, description: 'List of email templates' })
async findAll(@Query('page') page = 1, @Query('limit') limit = 10) {
return this.templateService.findAll(page, limit);
}
@@ -72,6 +74,8 @@ export class TemplateController {
*/
@Put(':id')
@ApiOperation({ summary: 'Update a template' })
+ @ApiResponse({ status: 200, description: 'Template updated successfully' })
+ @ApiResponse({ status: 404, description: 'Template not found' })
async update(
@Param('id', ParseUUIDPipe) id: string,
@Body() updateTemplateDto: UpdateTemplateDto,
@@ -86,6 +90,8 @@ export class TemplateController {
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: 'Delete a template' })
+ @ApiResponse({ status: 204, description: 'Template deleted successfully' })
+ @ApiResponse({ status: 404, description: 'Template not found' })
async remove(@Param('id', ParseUUIDPipe) id: string): Promise {
return this.templateService.remove(id);
}
@@ -97,6 +103,8 @@ export class TemplateController {
*/
@Post(':id/duplicate')
@ApiOperation({ summary: 'Duplicate a template' })
+ @ApiResponse({ status: 201, description: 'Template duplicated successfully' })
+ @ApiResponse({ status: 404, description: 'Template not found' })
async duplicate(@Param('id', ParseUUIDPipe) id: string): Promise {
return this.templateService.duplicate(id);
}
@@ -109,6 +117,8 @@ export class TemplateController {
*/
@Post(':id/preview')
@ApiOperation({ summary: 'Preview a template with sample data' })
+ @ApiResponse({ status: 201, description: 'Rendered template preview' })
+ @ApiResponse({ status: 404, description: 'Template not found' })
async preview(@Param('id', ParseUUIDPipe) id: string, @Body() sampleData?: Record) {
return this.templateService.previewTemplate(id, sampleData);
}
@@ -121,6 +131,8 @@ export class TemplateController {
*/
@Post(':id/render')
@ApiOperation({ summary: 'Render a template with provided variables' })
+ @ApiResponse({ status: 201, description: 'Rendered template output' })
+ @ApiResponse({ status: 404, description: 'Template not found' })
async render(@Param('id', ParseUUIDPipe) id: string, @Body() variables: Record) {
return this.templateService.renderTemplate(id, variables);
}
diff --git a/src/main.ts b/src/main.ts
index 190ba9fe..19053132 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -179,7 +179,13 @@ async function bootstrapWorker(): Promise {
.build();
const document = SwaggerModule.createDocument(app, config);
- SwaggerModule.setup('api', app, document);
+ SwaggerModule.setup('api/docs', app, document, {
+ swaggerOptions: {
+ persistAuthorization: true,
+ displayRequestDuration: true,
+ },
+ jsonDocumentUrl: 'api/docs-json',
+ });
const port = process.env.PORT || 3000;
app.enableShutdownHooks();
@@ -194,7 +200,7 @@ async function bootstrapWorker(): Promise {
}
logger.log(`TeachLink API running on http://localhost:${port}`);
- logger.log(`Swagger docs available at http://localhost:${port}/api`);
+ logger.log(`Swagger docs available at http://localhost:${port}/api/docs`);
logger.log(
`API versioning enabled via ${API_VERSION_HEADER}. Supported versions: ${SUPPORTED_API_VERSIONS.join(', ')}; default route version: ${DEFAULT_API_VERSION}.`,
);
diff --git a/src/search/search.controller.ts b/src/search/search.controller.ts
index 295966b4..7114d686 100644
--- a/src/search/search.controller.ts
+++ b/src/search/search.controller.ts
@@ -1,8 +1,10 @@
import { BadRequestException, Controller, Get, Query } from '@nestjs/common';
+import { ApiOperation, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger';
import { Throttle } from '@nestjs/throttler';
import { THROTTLE } from '../common/constants/throttle.constants';
import { SearchService } from './search.service';
+@ApiTags('Search')
@Throttle({ default: THROTTLE.SEARCH })
@Controller('search')
export class SearchController {
@@ -18,6 +20,32 @@ export class SearchController {
* @returns The operation result.
*/
@Get()
+ @ApiOperation({ summary: 'Search courses and learning content' })
+ @ApiQuery({ name: 'q', required: true, example: 'javascript basics' })
+ @ApiQuery({
+ name: 'filters',
+ required: false,
+ description: 'JSON encoded search filters',
+ example: '{"category":"programming","level":"beginner"}',
+ })
+ @ApiQuery({ name: 'sort', required: false, example: 'relevance' })
+ @ApiQuery({ name: 'page', required: false, example: 1 })
+ @ApiQuery({ name: 'limit', required: false, example: 20 })
+ @ApiResponse({
+ status: 200,
+ description: 'Search results',
+ schema: {
+ example: {
+ results: [],
+ total: 0,
+ page: 1,
+ limit: 20,
+ filters: { category: 'programming', level: 'beginner' },
+ query: 'javascript basics',
+ },
+ },
+ })
+ @ApiResponse({ status: 400, description: 'Invalid filters JSON' })
async search(
@Query('q') query: string,
@Query('filters') filters?: string,
@@ -41,6 +69,13 @@ export class SearchController {
}
@Get('autocomplete')
+ @ApiOperation({ summary: 'Get search autocomplete suggestions' })
+ @ApiQuery({ name: 'q', required: true, example: 'java' })
+ @ApiResponse({
+ status: 200,
+ description: 'Autocomplete suggestions',
+ schema: { example: ['javascript', 'java fundamentals', 'java spring'] },
+ })
async autocomplete(
@Query('q')
query: string,
@@ -49,11 +84,37 @@ export class SearchController {
}
@Get('filters')
+ @ApiOperation({ summary: 'Get available search filters' })
+ @ApiResponse({
+ status: 200,
+ description: 'Available filters',
+ schema: {
+ example: {
+ categories: ['programming', 'design'],
+ levels: ['beginner', 'intermediate'],
+ languages: ['en'],
+ priceRanges: [{ label: 'Free', lte: 0 }],
+ },
+ },
+ })
async getFilters(): Promise {
return this.searchService.getAvailableFilters();
}
@Get('analytics')
+ @ApiOperation({ summary: 'Get search analytics' })
+ @ApiQuery({ name: 'days', required: false, example: 7 })
+ @ApiResponse({
+ status: 200,
+ description: 'Search analytics summary',
+ schema: {
+ example: {
+ topQueries: [{ query: 'javascript', count: 42 }],
+ totalSearches: 120,
+ averageResults: 8.4,
+ },
+ },
+ })
async getAnalytics(
@Query('days')
days?: string,
diff --git a/src/security/secrets/secrets.controller.ts b/src/security/secrets/secrets.controller.ts
index 71981001..4f6ca0ea 100644
--- a/src/security/secrets/secrets.controller.ts
+++ b/src/security/secrets/secrets.controller.ts
@@ -1,5 +1,5 @@
import { Controller, Get, Post, Param, UseGuards } from '@nestjs/common';
-import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
+import { ApiTags, ApiOperation, ApiBearerAuth, ApiResponse } from '@nestjs/swagger';
import { SecretsManagerService } from './secrets-manager.service';
import { VaultSecretsService } from './vault-secrets.service';
import { Roles } from '../../auth/decorators/roles.decorator';
@@ -11,6 +11,8 @@ import { UserRole } from '../../users/entities/user.entity';
@Controller('secrets')
@UseGuards(JwtAuthGuard, RolesGuard)
@ApiBearerAuth()
+@ApiResponse({ status: 401, description: 'Authentication required' })
+@ApiResponse({ status: 403, description: 'Admin role required' })
export class SecretsController {
constructor(
private readonly secretsManagerService: SecretsManagerService,
@@ -20,6 +22,7 @@ export class SecretsController {
@Get('aws/:secretName')
@Roles(UserRole.ADMIN)
@ApiOperation({ summary: 'Get secret from AWS Secrets Manager' })
+ @ApiResponse({ status: 200, description: 'Secret lookup result with redacted value' })
async getAWSSecret(@Param('secretName') secretName: string) {
const value = await this.secretsManagerService.getSecret(secretName);
return { secretName, value: value ? '***REDACTED***' : null };
@@ -28,6 +31,7 @@ export class SecretsController {
@Get('vault/:secretName')
@Roles(UserRole.ADMIN)
@ApiOperation({ summary: 'Get secret from HashiCorp Vault' })
+ @ApiResponse({ status: 200, description: 'Vault secret lookup result with redacted value' })
async getVaultSecret(@Param('secretName') secretName: string) {
const value = await this.vaultSecretsService.getSecret(secretName);
return { secretName, value: value ? '***REDACTED***' : null };
@@ -36,6 +40,7 @@ export class SecretsController {
@Post('aws/rotate/:secretName')
@Roles(UserRole.ADMIN)
@ApiOperation({ summary: 'Rotate secret in AWS Secrets Manager' })
+ @ApiResponse({ status: 201, description: 'AWS secret rotated' })
async rotateAWSSecret(@Param('secretName') secretName: string) {
await this.secretsManagerService.rotateSecret(secretName);
return { message: `Secret ${secretName} rotated successfully` };
@@ -44,6 +49,7 @@ export class SecretsController {
@Post('vault/rotate/:secretName')
@Roles(UserRole.ADMIN)
@ApiOperation({ summary: 'Rotate secret in HashiCorp Vault' })
+ @ApiResponse({ status: 201, description: 'Vault secret rotated' })
async rotateVaultSecret(@Param('secretName') secretName: string) {
await this.vaultSecretsService.rotateSecret(secretName);
return { message: `Secret ${secretName} rotated successfully in Vault` };
@@ -52,6 +58,7 @@ export class SecretsController {
@Post('cache/clear')
@Roles(UserRole.ADMIN)
@ApiOperation({ summary: 'Clear secret cache' })
+ @ApiResponse({ status: 201, description: 'Secret cache cleared' })
async clearCache() {
this.secretsManagerService.clearCache();
return { message: 'Secret cache cleared' };
diff --git a/src/tenancy/tenancy.controller.ts b/src/tenancy/tenancy.controller.ts
index 3968c04c..c4e65b50 100644
--- a/src/tenancy/tenancy.controller.ts
+++ b/src/tenancy/tenancy.controller.ts
@@ -33,6 +33,8 @@ import { TenantPlan } from './entities/tenant.entity';
@Controller('tenants')
@UseGuards(JwtAuthGuard, RolesGuard)
@ApiBearerAuth()
+@ApiResponse({ status: 401, description: 'Authentication required' })
+@ApiResponse({ status: 403, description: 'Insufficient tenant permissions' })
export class TenancyController {
constructor(
private readonly tenancyService: TenancyService,
diff --git a/test/app.e2e-spec.ts b/test/app.e2e-spec.ts
index d0336e71..962901c0 100644
--- a/test/app.e2e-spec.ts
+++ b/test/app.e2e-spec.ts
@@ -1,59 +1,40 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
-import { ConfigModule } from '@nestjs/config';
-import { TypeOrmModule } from '@nestjs/typeorm';
import { AppModule } from '../src/app.module';
-import { TestDatabaseService } from './utils/test-database.service';
import { TestHttpClient } from './utils/test-http-client';
import { TestRetryHelper } from './utils/test-retry-helper';
describe('App (e2e)', () => {
let app: INestApplication;
- let testDb: TestDatabaseService;
let httpClient: TestHttpClient;
let retryHelper: TestRetryHelper;
beforeAll(async () => {
- // Initialize test database
- testDb = new TestDatabaseService();
- await testDb.setup();
-
- // Create test module with full app context
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
- })
- .overrideProvider('DATABASE_CONNECTION')
- .useValue(testDb.getConnection())
- .compile();
+ }).compile();
app = moduleFixture.createNestApplication();
-
- // Configure app for testing
- app.setGlobalPrefix('api');
await app.init();
- // Initialize test utilities
httpClient = new TestHttpClient(app.getHttpServer());
retryHelper = new TestRetryHelper();
}, 60000); // 60 second timeout for setup
afterAll(async () => {
- await app.close();
- await testDb.teardown();
+ if (app) {
+ await app.close();
+ }
}, 30000);
- beforeEach(async () => {
- // Clean database between tests
- await testDb.clean();
- });
-
describe('Health Check', () => {
- it('should return healthy status with retries', async () => {
+ it('should return app status with retries', async () => {
await retryHelper.withRetry(
async () => {
- const response = await httpClient.get('/health');
+ const response = await httpClient.get('/');
expect(response.status).toBe(200);
- expect(response.body).toHaveProperty('status', 'ok');
+ expect(response.body).toHaveProperty('message', 'TeachLink API is running');
+ expect(response.body).toHaveProperty('timestamp');
},
{
maxAttempts: 3,
@@ -62,20 +43,6 @@ describe('App (e2e)', () => {
},
);
}, 10000);
-
- it('should handle database connectivity', async () => {
- await retryHelper.withRetry(
- async () => {
- const response = await httpClient.get('/api/health/database');
- expect(response.status).toBe(200);
- expect(response.body).toHaveProperty('database', 'connected');
- },
- {
- maxAttempts: 5,
- delayMs: 500,
- },
- );
- }, 15000);
});
describe('API Endpoints', () => {
@@ -83,24 +50,26 @@ describe('App (e2e)', () => {
const requests = Array(10)
.fill(null)
.map(() =>
- retryHelper.withRetry(() => httpClient.get('/'), { maxAttempts: 3, delayMs: 200 }),
+ retryHelper.withRetry(() => httpClient.get('/search?q=javascript'), {
+ maxAttempts: 3,
+ delayMs: 200,
+ }),
);
const results = await Promise.all(requests);
results.forEach((response) => {
expect(response.status).toBe(200);
+ expect(response.body).toHaveProperty('query', 'javascript');
});
}, 30000);
- it('should handle request timeouts gracefully', async () => {
+ it('should return autocomplete suggestions endpoint', async () => {
await retryHelper.withRetry(
async () => {
- // Test with a potentially slow endpoint
- const response = await httpClient.get('/api/slow-endpoint', {
- timeout: 5000,
- });
- expect([200, 404]).toContain(response.status); // Either success or not found
+ const response = await httpClient.get('/search/autocomplete?q=java');
+ expect(response.status).toBe(200);
+ expect(Array.isArray(response.body)).toBe(true);
},
{
maxAttempts: 2,
diff --git a/test/jest-e2e.json b/test/jest-e2e.json
index 1031de56..374b835a 100644
--- a/test/jest-e2e.json
+++ b/test/jest-e2e.json
@@ -1,7 +1,7 @@
{
- "moduleFileExtensions": ["js", "json", "ts"],
+ "moduleFileExtensions": ["ts", "js", "json"],
"rootDir": ".",
- "testEnvironment": "./test/utils/test-environment.js",
+ "testEnvironment": "./utils/test-environment.js",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
@@ -16,7 +16,7 @@
"bail": false,
"reporters": [
"default",
- "./test/utils/flaky-test-detector.ts"
+ "./utils/flakiness-reporter.js"
],
"globals": {
"ts-jest": {
@@ -40,5 +40,5 @@
}
}
},
- "testSequencer": "./test/utils/test-sequencer.js"
+ "testSequencer": "./utils/test-sequencer.js"
}
diff --git a/test/utils/flakiness-reporter.js b/test/utils/flakiness-reporter.js
new file mode 100644
index 00000000..4c015e58
--- /dev/null
+++ b/test/utils/flakiness-reporter.js
@@ -0,0 +1,3 @@
+const { FlakinessReporter } = require('./flaky-test-detector.js');
+
+module.exports = FlakinessReporter;
diff --git a/test/utils/flaky-test-detector.ts b/test/utils/flaky-test-detector.ts
index 66efb981..2fa685ae 100644
--- a/test/utils/flaky-test-detector.ts
+++ b/test/utils/flaky-test-detector.ts
@@ -1,4 +1,3 @@
-import { TestResult } from '@jest/types';
import * as fs from 'fs';
import * as path from 'path';
@@ -20,7 +19,7 @@ export class FlakyTestDetector {
private readonly failureThreshold = 0.1; // 10% failure rate
private readonly minRunsThreshold = 3; // Need at least 3 runs to detect flakiness
- recordTestResult(testResult: TestResult.AssertionResult, testPath: string): void {
+ recordTestResult(testResult: any, testPath: string): void {
const testKey = `${testPath}:${testResult.title}`;
const existing = this.testResults.get(testKey) || {
diff --git a/test/utils/test-environment.js b/test/utils/test-environment.js
index f71ed636..25fc230e 100644
--- a/test/utils/test-environment.js
+++ b/test/utils/test-environment.js
@@ -1,4 +1,8 @@
-const NodeEnvironment = require('jest-environment-node');
+const NodeEnvironmentPackage = require('jest-environment-node');
+const NodeEnvironment =
+ NodeEnvironmentPackage.TestEnvironment ||
+ NodeEnvironmentPackage.default ||
+ NodeEnvironmentPackage;
class TestEnvironment extends NodeEnvironment {
constructor(config, context) {
@@ -141,14 +145,7 @@ class TestEnvironment extends NodeEnvironment {
}
// Utility method for tests to wait for conditions
- async waitForCondition(
- condition: () => boolean | Promise,
- options: {
- timeout?: number;
- interval?: number;
- description?: string;
- } = {},
- ): Promise {
+ async waitForCondition(condition, options = {}) {
const { timeout = 5000, interval = 100, description = 'condition' } = options;
const startTime = Date.now();
@@ -169,15 +166,7 @@ class TestEnvironment extends NodeEnvironment {
}
// Utility method for stable async operations
- async withRetry(
- operation: () => Promise,
- options: {
- maxAttempts?: number;
- delayMs?: number;
- backoffMultiplier?: number;
- retryCondition?: (error: any) => boolean;
- } = {},
- ): Promise {
+ async withRetry(operation, options = {}) {
const {
maxAttempts = 3,
delayMs = 1000,
@@ -185,7 +174,7 @@ class TestEnvironment extends NodeEnvironment {
retryCondition = (error) => error.code === 'ECONNREFUSED' || error.status >= 500,
} = options;
- let lastError: any;
+ let lastError;
let currentDelay = delayMs;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
@@ -208,4 +197,4 @@ class TestEnvironment extends NodeEnvironment {
}
}
-module.exports = TestEnvironment;
\ No newline at end of file
+module.exports = TestEnvironment;
diff --git a/test/utils/test-http-client.ts b/test/utils/test-http-client.ts
index 91cae7dd..04be2d96 100644
--- a/test/utils/test-http-client.ts
+++ b/test/utils/test-http-client.ts
@@ -20,7 +20,7 @@ export interface HttpResponse extends Response {
export class TestHttpClient {
private server: Server;
- private defaultTimeout = 10000;
+ private defaultTimeout: number | undefined;
private defaultRetries = 2;
private defaultRetryDelay = 500;
@@ -148,12 +148,22 @@ export class TestHttpClient {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
if (timeout) {
- return await Promise.race([
- operation(),
- new Promise((_, reject) =>
- setTimeout(() => reject(new Error(`Request timeout after ${timeout}ms`)), timeout),
- ),
- ]);
+ let timeoutId: NodeJS.Timeout | undefined;
+ try {
+ return await Promise.race([
+ operation(),
+ new Promise((_, reject) => {
+ timeoutId = setTimeout(
+ () => reject(new Error(`Request timeout after ${timeout}ms`)),
+ timeout,
+ );
+ }),
+ ]);
+ } finally {
+ if (timeoutId) {
+ clearTimeout(timeoutId);
+ }
+ }
}
return await operation();
} catch (error) {
diff --git a/tsconfig.json b/tsconfig.json
index 09806fba..2d92d3a4 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -24,7 +24,7 @@
"resolveJsonModule": true,
"esModuleInterop": true,
"lib": ["es2021", "dom"],
- "types": ["node"]
+ "types": ["node", "jest"]
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "test/**/*"]